Contents
, where is observation of , the number of Gaussian components, the mixing weight (, ), and the mean and covariance of component at the current iteration; , written in the M-step notes.
Deep Breakdown
What it tells me (Situation Awareness): A Gaussian mixture assumes each observation was produced by one of hidden components, but the label is never observed. The E-step fills that gap with probabilities: holding the current parameters fixed, it applies Bayes' theorem to every point and every component. The prior is the mixing weight (how common the component is), the likelihood is the Gaussian density (how well the component explains the point), and the denominator is the mixture density at , which makes each row of the responsibility matrix sum to . The result is a soft assignment: a point midway between two clusters gets, for example, and instead of a hard label. These responsibilities are the expected values of the hidden one-hot indicators, and the M-step uses them as weights to re-estimate , and . Alternating the two steps never decreases the log-likelihood. In DA/DS: responsibilities are the segment-membership probabilities reported to the business, the signal for flagging ambiguous customers (max responsibility below a threshold), the input to anomaly scoring (a low denominator means the point fits no component), and the soft counterpart of the K-means assignment step.
Flowchart
Real-world use
Where it is used
Every EM fit of a Gaussian mixture (sklearn.mixture.GaussianMixture, R mclust), where predict_proba returns exactly these responsibilities; customer and behavioural segmentation with overlapping groups; speaker identification and background models in audio; colour-based image segmentation; anomaly detection on the mixture density; and, with other component densities, latent class models, hidden Markov models (forward-backward is an E-step) and topic models.
Problem it solves
Fitting a mixture by maximum likelihood is hard because the log of a sum, , has no closed-form maximiser. If the component labels were known, each component could be fitted separately with ordinary weighted means and covariances. The E-step supplies the best available substitute for the missing labels: their posterior probabilities under the current model.
How it solves the problem
Bayes' theorem with and gives directly. With these weights fixed, the expected complete-data log-likelihood separates by component and has closed-form maximisers: the M-step. Because the E-step makes the lower bound on the log-likelihood touch the true log-likelihood at the current parameters, each EM iteration can only increase it or leave it unchanged.
Example in a business
An analyst segments 1,000 customers on two standardised behaviour scores (basket size, visit frequency) with a two-component mixture. After EM converges, the responsibilities give each customer a membership probability; the effective segment sizes are and . 17.5 % of customers have a maximum responsibility below : they sit in the overlap, so the CRM team sends them the blended offer instead of forcing a hard label that carries more than a 10 % chance of being wrong for each of them.
The formula, piece by piece
- : responsibility of component for point ; in , and for every
- : prior probability of component ; a large pulls ambiguous points towards
- : the component likelihood in dimensions; falls with the Mahalanobis distance from
- : the joint
- : the mixture density ; its logarithm summed over is the log-likelihood that EM increases
- : effective number of points in component ;
- Hard limit: if all , the E-step reduces to the K-means assignment step
- Equal-density case: when two components have equal density at , , the prior alone decides
The formula in words
"For each point, weight every component's density at that point by how common the component is, then divide by the total so the weights add up to one; the result is the probability that the point came from each component." Tiny example: with and unit-variance components at and , the point is equally far from both means, so the densities are equal and the responsibilities are just the weights, and .
Critical Concepts
- It is Bayes' theorem: prior times likelihood, divided by the evidence . See Bayes' Theorem and Multivariate Gaussian Distribution.
- Compute in log space: in many dimensions or for far-away points every density underflows to and gives NaN. Compute , subtract the row's log-sum-exp, then exponentiate. Every library does this. See Log-Sum-Exp Trick (prevents underflowoverflow).
- Soft versus hard: K-means assigns each point wholly to the nearest centre; the E-step splits it in proportion to posterior probability, and the covariance makes "nearest" mean Mahalanobis rather than Euclidean distance. See K-Means Objective, Within-Cluster Sum of Squares, WCSS.
- Feeds every M-step update: , , . See Effective Component Size, M-Step (Weights Update), M-Step (Means Update) and M-Step (Covariance Update).
- Monotone log-likelihood: each E-step plus M-step never decreases ; a decrease in practice signals a bug, usually in the E-step normalisation. See Log-Likelihood Objective and Latent Variable Expected Log-Likelihood.
- Collapse risk: a component whose responsibilities concentrate on one point drives and the likelihood to infinity; a small ridge on the covariance (
reg_covar) prevents it. - Local optima: responsibilities depend on the starting parameters; several initialisations (
n_init) and BIC-based choice of are standard. The mixture itself is described in Mixture Density.
Step-by-step calculation (tiny example)
Two 1-D components: , , . Responsibilities for .
Step 1: Component densities ; .
Step 2: Weight by the priors ; .
Step 3: Mixture density (denominator) .
Step 4: Normalise ; ; the two sum to .
Step 5: Read it The point is one unit from component 1 and two units from component 2; the density ratio is and the prior ratio , so the posterior odds are , i.e. for component 1.
Code examples
Beginner example (Python)
import numpy as np
from scipy.stats import norm
# Two 1-D Gaussian components: weights, means, standard deviations
pi = np.array([0.6, 0.4])
mu = np.array([0.0, 3.0])
sd = np.array([1.0, 1.0])
x = np.array([-1.0, 1.0, 1.5, 2.0, 4.0])
# E-step: weighted density of each point under each component ...
weighted = pi * norm.pdf(x[:, None], mu, sd) # shape (n, K)
# ... normalised across components so each row sums to 1
gamma = weighted / weighted.sum(axis=1, keepdims=True)
print(" x pi*N(x|comp 1) pi*N(x|comp 2) gamma_1 gamma_2")
for xi, w, g in zip(x, weighted, gamma):
print(f"{xi:>4.1f} {w[0]:>13.6f} {w[1]:>14.6f} {g[0]:>7.4f} {g[1]:>7.4f}")
print("\nRows sum to 1:", np.allclose(gamma.sum(axis=1), 1))
print("Effective counts N_k = sum_i gamma_ik:", np.round(gamma.sum(axis=0), 4))
# Log-likelihood of the data under the current parameters (EM never decreases it)
print("log-likelihood:", round(float(np.log(weighted.sum(axis=1)).sum()), 4))Beginner example (R)
pi_k <- c(0.6, 0.4)
mu <- c(0, 3)
sd_k <- c(1, 1)
x <- c(-1, 1, 1.5, 2, 4)
# n x K matrix of pi_k * N(x_i | mu_k, sd_k)
weighted <- sapply(1:2, function(k) pi_k[k] * dnorm(x, mu[k], sd_k[k]))
gamma <- weighted / rowSums(weighted)
print(round(cbind(x = x, gamma_1 = gamma[, 1], gamma_2 = gamma[, 2]), 4))
print(rowSums(gamma)) # all 1
print(round(colSums(gamma), 4)) # effective counts N_k
# Log-space version: stable when densities underflow
log_w <- sapply(1:2, function(k) log(pi_k[k]) + dnorm(x, mu[k], sd_k[k], log = TRUE))
row_max <- apply(log_w, 1, max)
log_norm <- row_max + log(rowSums(exp(log_w - row_max)))
print(all.equal(exp(log_w - log_norm), gamma)) # TRUEProfessional example (Python)
import warnings
import numpy as np
from scipy.special import logsumexp
from scipy.stats import multivariate_normal
from sklearn.exceptions import ConvergenceWarning
from sklearn.mixture import GaussianMixture
rng = np.random.default_rng(3)
def e_step(X, weights, means, covs):
"""Responsibilities in log space: stable for far-away points and high dimensions."""
log_w = np.column_stack([
np.log(weights[k]) + multivariate_normal.logpdf(X, means[k], covs[k])
for k in range(len(weights))
]) # (n, K): ln pi_k + ln N(x_i | mu_k, Sigma_k)
log_norm = logsumexp(log_w, axis=1, keepdims=True)
return np.exp(log_w - log_norm), float(log_norm.sum())
def m_step(X, gamma):
nk = gamma.sum(axis=0)
weights = nk / X.shape[0]
means = (gamma.T @ X) / nk[:, None]
covs = []
for k in range(gamma.shape[1]):
d = X - means[k]
covs.append((gamma[:, k, None] * d).T @ d / nk[k] + 1e-6 * np.eye(X.shape[1]))
return weights, means, np.array(covs)
# --- Data: two overlapping customer segments in 2-D --------------------------
X = np.vstack([
rng.multivariate_normal([0, 0], [[1.0, 0.3], [0.3, 1.0]], 600),
rng.multivariate_normal([2.5, 1.5], [[0.8, -0.2], [-0.2, 0.6]], 400),
])
# --- EM from a deliberately poor start ---------------------------------------
weights = np.array([0.5, 0.5])
means = X[rng.choice(len(X), 2, replace=False)]
covs = np.array([np.eye(2), np.eye(2)])
history = []
for it in range(200):
gamma, ll = e_step(X, weights, means, covs)
history.append(ll)
if it > 0 and ll - history[-2] < 1e-8:
break
weights, means, covs = m_step(X, gamma)
print(f"EM stopped after {len(history)} E-steps, log-likelihood {history[0]:.2f} -> {history[-1]:.2f}")
print("log-likelihood never decreased:", bool(np.all(np.diff(history) >= -1e-9)))
order = np.argsort(means[:, 0])
print("weights:", np.round(weights[order], 3), " means:", np.round(means[order], 3).tolist())
# --- Agreement with scikit-learn's E-step (predict_proba) ---------------------
with warnings.catch_warnings():
warnings.simplefilter("ignore", ConvergenceWarning) # max_iter=1 is deliberate
gm = GaussianMixture(2, covariance_type="full", reg_covar=1e-6, means_init=means,
weights_init=weights, precisions_init=np.linalg.inv(covs), max_iter=1).fit(X)
gamma_ours, _ = e_step(X, gm.weights_, gm.means_, gm.covariances_)
print("max |gamma_ours - sklearn| =", f"{np.abs(gamma_ours - gm.predict_proba(X)).max():.2e}")
# --- Ambiguity summary: how soft are the assignments? -------------------------
max_gamma = gamma.max(axis=1)
print(f"points with max responsibility < 0.9: {np.mean(max_gamma < 0.9):.1%}")
print(f"effective counts N_k: {np.round(gamma.sum(axis=0)[order], 1)}")
# --- Why log space: a far point in 50 dimensions ------------------------------
D = 50
far = np.full((1, D), 8.0)
w2, m2, c2 = np.array([0.5, 0.5]), np.array([np.zeros(D), np.ones(D)]), np.array([np.eye(D)] * 2)
with np.errstate(invalid="ignore"):
dens = np.array([w2[k] * multivariate_normal.pdf(far, m2[k], c2[k]) for k in range(2)])
naive = dens / dens.sum()
print(f"\nnaive densities: {dens}, naive gamma: {naive}")
print("log-space gamma:", e_step(far, w2, m2, c2)[0].ravel())From a start at two random data points with identity covariances, EM stops after 79 E-steps, raising the log-likelihood from to without a single decrease. The fitted weights and means , are close to the generating , , . The log-space E-step matches scikit-learn's predict_proba to . 17.5 % of points have no component with responsibility of at least . In 50 dimensions, the point at has both densities underflow to , so the naive ratio is NaN; the log-space E-step returns and , since the squared distances to the two means differ by and .
Source: domain knowledge