USAAIO Lesson 53, from Week 19 of Phase 3. It covers the GMM density model as K weighted Gaussians, then derives the E-step soft assignments and the M-step parameter updates from the expected complete-data log-likelihood, along with the convergence guarantees of EM. It covers the covariance types and model selection by BIC, compares a GMM with k-Means on non-spherical data, and ends with GMM-based anomaly detection. You build a GMM from scratch with NumPy and SciPy and validate it against sklearn. The lesson runs to 32 slides.
Subject: Machine Learning · 63 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
USAAIO · Lesson 53 · Week 19 (Phase 3)
Soft assignments, covariance shapes, non-convex log-likelihood. Today: derive the E-step and M-step from first principles, implement EM from scratch, and detect anomalies with score_samples.
Objectives
p(x) = sum_k pi_k * N(x; mu_k, Sigma_k) and explain each termr_{ik} from Bayes' rulesklearn.mixture.GaussianMixturescore_samples for anomaly detection and BIC for selecting KWarm-up
Discussion prompt
Before we open Lesson 53: Gaussian Mixture Models & the EM Algorithm: without looking back, what was the main idea of k-Means Clustering, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
k-Means assign-then-update, k-Means++ initialization, elbow method and WCSS, silhouette score, EM interpretation (E=assign / M=centroid update), soft k-Means as GMM, and k-Means failure on non-spherical clusters fixed by DBSCAN. Build k-Means from scratch with random and k++ init, implement elbow method and silhouette from scratch, and verify convergence on make_blobs.
Section
Part 1 of 4
Concept
A GMM is a weighted sum of K Gaussian components. Each component k has weight pi_k, mean mu_k, and covariance Sigma_k.
\[ p(x) = \sum_{k=1}^{K} \pi_k \, \mathcal{N}(x;\, \mu_k,\, \Sigma_k) \]
Constraints: pi_k >= 0 and sum_k pi_k = 1. Compared to k-Means (Lesson 21), GMM is a probabilistic model: it assigns fractional membership and outputs a density, not just labels.
Counterexample
Discussion prompt
A GMM is a weighted sum of K Gaussian components. Each component k has weight pi_k, mean mu_k, and covariance Sigma_k.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Constraints: pi_k >= 0 and sum_k pi_k = 1. Compared to k-Means (Lesson 21), GMM is a probabilistic model: it assigns fractional membership and outputs a density, not just labels.
Concept
| type | shape | parameters (D=2) |
|---|---|---|
| spherical | sigma^2 * I (same radius all dims) | 1 per component |
| diagonal | diag(sigma_1^2, ..., sigma_D^2) | D per component |
| tied | one shared Sigma for all k | D*(D+1)/2 total |
| full | separate Sigma_k per component | D*(D+1)/2 per component |
Full covariance lets each component be an arbitrary ellipse. The price is more parameters — use BIC to select type and K simultaneously (see Part 4).
Comparison
Comparison matrix
From Covariance types — four flavors: refill the shape column from what you know. The rest of the table is as it appeared.
| type | shape | parameters (D=2) |
|---|---|---|
| spherical | sigma^2 * I (same radius all dims) | 1 per component |
| diagonal | diag(sigma_1^2, ..., sigma_D^2) | D per component |
| tied | one shared Sigma for all k | D*(D+1)/2 total |
| full | separate Sigma_k per component | D*(D+1)/2 per component |
Intuition
K-Means (Lesson 21) commits: every point is 100% in one cluster. A point near the boundary gets assigned exactly as confidently as a point at the center.
GMM instead asks: given the current parameters, what fraction of this point's density comes from each component? A boundary point might be 51% component 1, 49% component 2.
| x | GMM r_left | GMM r_right | k-Means hard |
|---|---|---|---|
| -2.0 | 1.0000 | 0.0000 | 0 |
| +0.0 | 0.5103 | 0.4897 | 0 |
| +2.0 | 0.0000 | 1.0000 | 1 |
At x=0 (midpoint), k-Means flips a coin once and locks in; GMM gives near-equal probability to both. Those soft weights drive the M-step updates.
Trade off
Comparison matrix
From Why soft assignments beat hard ones: every row here is a choice with a cost. Fill the k-Means hard column, then say which row you would actually pick and what you give up for it.
| x | GMM r_left | GMM r_right | k-Means hard |
|---|---|---|---|
| -2.0 | 1.0000 | 0.0000 | 0 |
| +0.0 | 0.5103 | 0.4897 | 0 |
| +2.0 | 0.0000 | 1.0000 | 1 |
Section
Part 2 of 4
Concept
Given current parameters, the responsibility r_{ik} is the posterior probability that component k generated point i — just Bayes' rule.
\[ r_{ik} = \frac{\pi_k\,\mathcal{N}(x_i;\,\mu_k,\,\Sigma_k)}{\sum_{j=1}^{K}\pi_j\,\mathcal{N}(x_i;\,\mu_j,\,\Sigma_j)} \]
The denominator is exactly p(x_i) — marginalizing over all components. Computing this for all N points is the E (expectation) step.
Analogy
Discussion prompt
Explain E-step: posterior responsibility by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Given current parameters, the responsibility r_{ik} is the posterior probability that component k generated point i — just Bayes' rule.
Ranking
Put in order
Put the moves of E-step by hand (K=2, 1-D) into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. The left component's PDF at x=0.5 is tiny: 2.5 standard deviations away.
Worked example
Fix pi = [0.35, 0.65], mu = [-2.0, +2.0], sigma = 0.7 for both. Evaluate r at x = +0.5.
Compute pi_1 * N(0.5; -2.0, 0.7) = 0.35 * 0.00115 = 0.000402
Why: The left component's PDF at x=0.5 is tiny: 2.5 standard deviations away.
Compute pi_2 * N(0.5; +2.0, 0.7) = 0.65 * 0.10917 = 0.070960
Why: The right component's PDF at x=0.5 is much larger: only 2.14 std devs away and has higher weight.
p(x=0.5) = 0.000402 + 0.070960 = 0.071362; r1 = 0.0056, r2 = 0.9944
Why: The point is 99.4% attributed to the right component. This soft weight is what gets passed to the M-step.
| x | pi1*N (left) | pi2*N (right) | r_left | r_right |
|---|---|---|---|---|
| -2.0 | 0.19947 | ~0 | 1.0000 | 0.0000 |
| +0.0 | 0.00337 | 0.00625 | 0.3500 | 0.6500 |
| +2.0 | ~0 | 0.37045 | 0.0000 | 1.0000 |
Pattern
Step through it
Step through E-step by hand (K=2, 1-D) one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 3 of 4
Concept
Given responsibilities r_{ik}, we update each parameter to the weighted maximum-likelihood estimate, treating r_{ik} as soft counts.
\[ N_k = \sum_{i=1}^{N} r_{ik} \qquad \pi_k^\text{new} = \frac{N_k}{N} \qquad \mu_k^\text{new} = \frac{\sum_i r_{ik}\, x_i}{N_k} \]
\[ \Sigma_k^\text{new} = \frac{\sum_i r_{ik}\,(x_i - \mu_k^\text{new})(x_i - \mu_k^\text{new})^\top}{N_k} \]
Compare to k-Means M-step (Lesson 21): those updates are identical but with r_{ik} in {0,1}. EM generalizes k-Means by allowing fractional membership.
Explain it
Discussion prompt
Explain M-step: weighted MLE to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Given responsibilities r_{ik}, we update each parameter to the weighted maximum-likelihood estimate, treating r_{ik} as soft counts.
Ranking
Put in order
Put the moves of M-step by hand (N=4 toy) into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Effective count for each cluster — not an integer because assignments are soft.
Worked example
Four points xs = [-2.5, -1.8, +1.5, +2.3]. After E-step, responsibilities for component 1 are r = [0.92, 0.88, 0.05, 0.03].
N_k1 = 0.92 + 0.88 + 0.05 + 0.03 = 1.88; N_k2 = 4 - 1.88 = 2.12
Why: Effective count for each cluster — not an integer because assignments are soft.
mu_k1_new = (0.92-2.5 + 0.88-1.8 + 0.051.5 + 0.032.3) / 1.88 = -1.9894
Why: Points with high responsibility pull the mean strongly; low-responsibility points barely contribute.
pi_k1_new = 1.88 / 4 = 0.4700; pi_k2_new = 2.12 / 4 = 0.5300
Why: New mixing weights are just the fraction of total soft count each component holds.
| parameter | component 1 | component 2 |
|---|---|---|
| N_k (soft count) | 1.88 | 2.12 |
| mu_k (new mean) | -1.9894 | +1.5283 |
| pi_k (new weight) | 0.4700 | 0.5300 |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
pi_k1_new = 1.88 / 4 = 0.4700; pi_k2_new = 2.12 / 4 = 0.5300
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Four points xs = [-2.5, -1.8, +1.5, +2.3]. After E-step, responsibilities for component 1 are r = [0.92, 0.88, 0.05, 0.03].
Anomaly
Predict first
A student writes this, and it looks reasonable:
The GMM log-likelihood is non-convex, but EM is guaranteed to converge — so it finds the global maximum.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: EM guarantees the log-likelihood never decreases, so it converges — but only to a local maximum.
EM guarantees monotone non-decreasing log-likelihood, not the global maximum.
Why: EM guarantees the log-likelihood never decreases, so it converges — but only to a local maximum. The non-convex landscape has many local optima, and a bad initialization can land in a poor one.
Trap
The GMM log-likelihood is non-convex, but EM is guaranteed to converge — so it finds the global maximum.
Run EM once from a single random initialization and accept whatever it converges to
Why: EM guarantees the log-likelihood never decreases, so it converges — but only to a local maximum. The non-convex landscape has many local optima, and a bad initialization can land in a poor one.
EM guarantees monotone non-decreasing log-likelihood, not the global maximum.
Run EM with multiple restarts (n_init > 1) and keep the solution with the highest final log-likelihood
Why: sklearn defaults to n_init=1 — set n_init=10 for robustness. Initialization with k-Means (the sklearn default init='kmeans') also helps avoid degenerate starts.
Break the constraint
Discussion prompt
The rule this trap just fixed:
EM guarantees monotone non-decreasing log-likelihood, not the global maximum.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
EM guarantees the log-likelihood never decreases, so it converges — but only to a local maximum. The non-convex landscape has many local optima, and a bad initialization can land in a poor one.
Estimation
Predict first
Run GaussianMixture with max_iter capped at increasing values and read off the log-likelihood after each cap.
Commit before you compute: what does EM convergence trace (sklearn) come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Iter 1: -1120.32, iter 2: -1119.92, iter 3: -1119.77, iter 5: -1119.67, iter 10: -1119.67 (converged)
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The log-likelihood is strictly non-decreasing and flattens after ~5 iterations on this clean data.
Worked example
Run GaussianMixture with max_iter capped at increasing values and read off the log-likelihood after each cap.
from sklearn.mixture import GaussianMixture
from sklearn.datasets import make_blobs
import numpy as np
X, _ = make_blobs(300, 2, centers=3, cluster_std=1.0, random_state=0)
for it in [1, 2, 3, 5, 10, 20]:
gm = GaussianMixture(3, covariance_type='full', random_state=0, max_iter=it)
gm.fit(X)
print(f'iter={it:3d}: LL={gm.score(X)*len(X):.2f}')Iter 1: -1120.32, iter 2: -1119.92, iter 3: -1119.77, iter 5: -1119.67, iter 10: -1119.67 (converged)
Why: The log-likelihood is strictly non-decreasing and flattens after ~5 iterations on this clean data. Real datasets may need 50-200 iterations.
| max_iter | log-likelihood | delta |
|---|---|---|
| 1 | -1120.32 | -- |
| 2 | -1119.92 | +0.40 |
| 3 | -1119.77 | +0.15 |
| 5 | -1119.67 | +0.10 |
| 10 | -1119.67 | 0.00 (converged) |
Sorting
Sort into buckets
These are the pieces of Lesson 53: Gaussian Mixture Models & the EM Algorithm, out of order. Put each one back under the part of the lesson it belongs to.
Section
Part 4 of 4
Concept
The entire EM algorithm is: initialize, then alternate E-step (compute R) and M-step (update mus, covs, pis) until convergence.
Sigma_k = I, pi_k = 1/KR[:,k] = pi_k * N(X; mu_k, Sigma_k); normalize rowsN_k = R.sum(0); mu_k = (R[:,k] @ X) / N_k; Sigma_k = weighted_cov; pi_k = N_k/NExplain it
Discussion prompt
Explain From-scratch EM: the 20-line loop to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
The entire EM algorithm is: initialize, then alternate E-step (compute R) and M-step (update mus, covs, pis) until convergence.
Estimation
Predict first
K=2, make_blobs(200, centers=2, std=0.8, seed=0). Initialize means at two random data points; covs = I; pis = [0.5, 0.5]. Track LL across iterations.
Commit before you compute: what does EM from scratch — iteration trace come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: After 20 iterations: means = [[0.964, 4.356], [1.954, 0.762]] — matches sklearn (differs by <0.01)
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The from-scratch loop converges to the same local optimum as sklearn's optimized C++ backend, confirming the math is correct.
Worked example
K=2, make_blobs(200, centers=2, std=0.8, seed=0). Initialize means at two random data points; covs = I; pis = [0.5, 0.5]. Track LL across iterations.
import numpy as np
from scipy.stats import multivariate_normal as mvn
from sklearn.datasets import make_blobs
X, _ = make_blobs(200, 2, centers=2, cluster_std=0.8, random_state=0)
K, N, D = 2, *X.shape
rng = np.random.default_rng(0)
mus = X[rng.choice(N, K, replace=False)].astype(float)
covs = [np.eye(D)] * K
pis = np.ones(K) / K
for it in range(1, 21):
R = np.column_stack([pis[k]*mvn.pdf(X, mus[k], covs[k]) for k in range(K)])
R /= R.sum(1, keepdims=True)
Nk = R.sum(0)
mus = (R.T @ X) / Nk[:, None]
covs = [(R[:, k:k+1] * (X-mus[k])).T @ (X-mus[k]) / Nk[k] for k in range(K)]
pis = Nk / N
print(np.round(mus, 3))After 20 iterations: means = [[0.964, 4.356], [1.954, 0.762]] — matches sklearn (differs by <0.01)
Why: The from-scratch loop converges to the same local optimum as sklearn's optimized C++ backend, confirming the math is correct.
| iter | LL | notes |
|---|---|---|
| 1 | -605.86 | initial step, large jump |
| 2 | -602.33 | +3.53 |
| 3 | -601.34 | +0.99 |
| 5 | -601.04 | +0.30 |
| 10 | -601.03 | ~converged |
| 20 | -601.03 | same as sklearn final |
Concept
K-Means implicitly assumes spherical clusters of equal size. It assigns each point to the nearest centroid using Euclidean distance, so elongated or correlated clusters mislead it.
| method | assignment type | covariance | accuracy (oblique clusters) |
|---|---|---|---|
| k-Means | hard (0 or 1) | spherical only | 0.920 |
| GMM spherical | soft | spherical | 0.935 |
| GMM full-cov | soft | arbitrary ellipse | 0.970 |
On data from two oblique correlated Gaussians (off-diagonal covariance), GMM full-cov gains 5 percentage points over k-Means. The gap widens as the correlation grows stronger (Lesson 21 comparison).
Comparison
Comparison matrix
From GMM vs k-Means: when does GMM win?: refill the assignment type column from what you know. The rest of the table is as it appeared.
| method | assignment type | covariance | accuracy (oblique clusters) |
|---|---|---|---|
| k-Means | hard (0 or 1) | spherical only | 0.920 |
| GMM spherical | soft | spherical | 0.935 |
| GMM full-cov | soft | arbitrary ellipse | 0.970 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
diagonal covariance is cheaper than full and still models different variances per feature — it should work fine on any blob-like clusters.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Diagonal covariance forces the fitted ellipses to be axis-aligned — it cannot represent off-diagonal correlation.
Use covariance_type='full' when the data may have correlations between features.
Why: Diagonal covariance forces the fitted ellipses to be axis-aligned — it cannot represent off-diagonal correlation. On data generated by a full covariance matrix, it makes the same error as spherical, just with different axis scales.
Trap
diagonal covariance is cheaper than full and still models different variances per feature — it should work fine on any blob-like clusters.
Set covariance_type='diag' and trust it to fit correlated clusters
Why: Diagonal covariance forces the fitted ellipses to be axis-aligned — it cannot represent off-diagonal correlation. On data generated by a full covariance matrix, it makes the same error as spherical, just with different axis scales.
Use covariance_type='full' when the data may have correlations between features.
Select covariance type by BIC: lower BIC = better model after penalizing parameter count
Why: BIC automatically trades fit quality against parameter count. On the oblique-cluster data, full wins on BIC even though it has more parameters — the fit improvement outweighs the penalty.
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
pi_k, mean mu_k, and covariance Sigma_k.; K-Means (Lesson 21) commits: every point is 100% in one cluster. A point near the boundary gets assigned exactly as confidently as a point at the center.; Given current parameters, the responsibility r_{ik} is the posterior probability that component k generated point i — just Bayes' rule.diagonal covariance is cheaper than full and still models different variances per feature — it should work fine on any blob-like clusters.Estimation
Predict first
Fit GMM with K = 1 through 6 (full covariance) on make_blobs(300, centers=3, std=0.8, seed=42). The true K=3 should win on BIC.
Commit before you compute: what does BIC for selecting K and covariance type come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: K=1: 3753.0, K=2: 2600.0, K=3: 2150.3 (minimum), K=4: 2171.7, K=5: 2202.3
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. BIC drops steeply from K=1 to K=3 (large fit gain) then rises (penalty exceeds marginal fit gain).
Worked example
Fit GMM with K = 1 through 6 (full covariance) on make_blobs(300, centers=3, std=0.8, seed=42). The true K=3 should win on BIC.
from sklearn.mixture import GaussianMixture
from sklearn.datasets import make_blobs
X, _ = make_blobs(300, 2, centers=3, cluster_std=0.8, random_state=42)
for k in range(1, 7):
gm = GaussianMixture(k, covariance_type='full', random_state=0)
gm.fit(X)
print(f'K={k}: BIC={gm.bic(X):.1f}')K=1: 3753.0, K=2: 2600.0, K=3: 2150.3 (minimum), K=4: 2171.7, K=5: 2202.3
Why: BIC drops steeply from K=1 to K=3 (large fit gain) then rises (penalty exceeds marginal fit gain). The elbow at K=3 correctly identifies the true number of components.
| K | BIC | verdict |
|---|---|---|
| 1 | 3753.0 | underfit |
| 2 | 2600.0 | still underfit |
| 3 | 2150.3 | MINIMUM — best K |
| 4 | 2171.7 | overfit |
| 5 | 2202.3 | overfit |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
K=1: 3753.0, K=2: 2600.0, K=3: 2150.3 (minimum), K=4: 2171.7, K=5: 2202.3
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Fit GMM with K = 1 through 6 (full covariance) on make_blobs(300, centers=3, std=0.8, seed=42). The true K=3 should win on BIC.
Concept
Once fitted, gm.score_samples(X) returns log p(x_i) for each point. Anomalies are points with very low density — far from all component means.
A simple threshold: flag any point below the 5th percentile of training log-probabilities. On iris (150 samples), this flags 8 points whose log p(x) < -4.398.
| sample | log p(x) | class |
|---|---|---|
| idx 0 (typical setosa) | 1.571 | normal |
| idx 118 (worst-case) | -7.062 | anomaly |
| 5th pct threshold | -4.398 | boundary |
Analogy
Discussion prompt
Explain Anomaly detection with score_samples by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Once fitted, gm.score_samples(X) returns log p(x_i) for each point. Anomalies are points with very low density — far from all component means.
Ranking
Put in order
These are the steps of The GMM / EM recipe, scrambled. Put them back in order before the next slide shows you.
p(x) = sum_k pi_k * N(x; mu_k, Sigma_k), one row in the GMM densityr_{ik} = pi_k N(x_i; mu_k, Sig_k) / p(x_i) — Bayes posterior over componentsmu_k = sum_i(r_{ik} x_i) / N_k, Sigma_k similarlyn_init >= 5) for robustnessscore_samples(X) < thresholdWhy: This is the order the recipe itself gives. Recalling the sequence without the slide in front of you is the difference between recognising the method and being able to run it — most of what goes wrong in practice is a step done out of turn.
Pattern
p(x) = sum_k pi_k * N(x; mu_k, Sigma_k), one row in the GMM densityr_{ik} = pi_k N(x_i; mu_k, Sig_k) / p(x_i) — Bayes posterior over componentsmu_k = sum_i(r_{ik} x_i) / N_k, Sigma_k similarlyn_init >= 5) for robustnessscore_samples(X) < thresholdEdge cases
Discussion prompt
The GMM / EM recipe works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".
Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.
Answer:
p(x) = sum_k pi_k * N(x; mu_k, Sigma_k), one row in the GMM densityr_{ik} = pi_k N(x_i; mu_k, Sig_k) / p(x_i) — Bayes posterior over componentsmu_k = sum_i(r_{ik} x_i) / N_k, Sigma_k similarlyn_init >= 5) for robustnessscore_samples(X) < thresholdElimination
Eliminate the wrong options
In the E-step, r_{ik} is the responsibility of component k for point i. What quantity does the denominator sum_j pi_j N(x_i; mu_j, Sigma_j) equal?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: Summing pi_j * N(x_i; mu_j, Sigma_j) over all j marginalizes out the latent component assignment — this is exactly the GMM marginal density p(x_i). Dividing by p(x_i) turns the joint pi_k*N into a posterior (Bayes' theorem).
Check
Work through it before clicking.
Check your understanding
In the E-step, r_{ik} is the responsibility of component k for point i. What quantity does the denominator sum_j pi_j N(x_i; mu_j, Sigma_j) equal?
Answer: A
Why: Summing pi_j * N(x_i; mu_j, Sigma_j) over all j marginalizes out the latent component assignment — this is exactly the GMM marginal density p(x_i). Dividing by p(x_i) turns the joint pi_k*N into a posterior (Bayes' theorem).
Prediction
Predict first
After the E-step on N=200 points with K=3, the soft counts are N_1=72, N_2=83, N_3=45. What is the new mixing weight pi_3?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: 0.225
Why: pi_k_new = N_k / N = 45 / 200 = 0.225. The M-step weight update is just the fraction of total soft count that component k holds.
Check
Trace the update rule.
Check your understanding
After the E-step on N=200 points with K=3, the soft counts are N_1=72, N_2=83, N_3=45. What is the new mixing weight pi_3?
Answer: A
Why: pi_k_new = N_k / N = 45 / 200 = 0.225. The M-step weight update is just the fraction of total soft count that component k holds.
Elimination
Eliminate the wrong options
Which statement about EM for GMM is TRUE?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: EM's convergence theorem (Dempster, Laird, Rubin 1977) guarantees log-likelihood is non-decreasing at every step. However, the GMM log-likelihood is non-convex, so EM can stop at a local maximum. Multiple restarts are the standard mitigation.
Check
Key property of EM.
Check your understanding
Which statement about EM for GMM is TRUE?
Answer: A
Why: EM's convergence theorem (Dempster, Laird, Rubin 1977) guarantees log-likelihood is non-decreasing at every step. However, the GMM log-likelihood is non-convex, so EM can stop at a local maximum. Multiple restarts are the standard mitigation.
Section
Project
Concept
Implement GMM EM from scratch in NumPy/SciPy, validate it against sklearn, then use it for anomaly detection on the iris dataset.
| # | milestone | tool |
|---|---|---|
| 1 | E-step: compute soft responsibility matrix R | scipy.stats.multivariate_normal |
| 2 | M-step: update mus, covs, pis from R | numpy weighted averages |
| 3 | Iterate 20 steps; print final means + compare to sklearn | GaussianMixture |
| 4 | Anomaly detection on iris: flag bottom-5% log-probs | gm.score_samples |
Build rules: write E-step and M-step as separate functions; never divide before checking R.sum(1) is nonzero; add 1e-300 inside log to prevent -inf.
Counterexample
Discussion prompt
Implement GMM EM from scratch in NumPy/SciPy, validate it against sklearn, then use it for anomaly detection on the iris dataset.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Build rules: write E-step and M-step as separate functions; never divide before checking R.sum(1) is nonzero; add 1e-300 inside log to prevent -inf.
Worked example
Your turn: write e_step(X, mus, covs, pis) returning R, then m_step(X, R) returning new mus, covs, pis.
Hint: E-step — stack component densities column-wise, then divide each row by its sum. M-step — use Nk = R.sum(0); new mu_k = R[:,k] @ X / Nk[k].
import numpy as np
from scipy.stats import multivariate_normal as mvn
def e_step(X, mus, covs, pis):
R = np.column_stack([pis[k]*mvn.pdf(X, mus[k], covs[k]) for k in range(len(mus))])
R /= R.sum(1, keepdims=True)
return R
def m_step(X, R):
Nk = R.sum(0)
mus = (R.T @ X) / Nk[:, None]
covs = [(R[:, k:k+1]*(X-mus[k])).T@(X-mus[k])/Nk[k] for k in range(R.shape[1])]
return mus, covs, Nk / len(X)
print('functions defined')| function | input | output |
|---|---|---|
| e_step | X (N,D), mus, covs, pis | R (N,K) soft assignments |
| m_step | X (N,D), R (N,K) | updated mus, covs, pis |
Worked example
Your turn: initialize randomly, run 20 EM iterations, print final means. Predict whether they match sklearn.
Hint: seed np.random.default_rng(0); pick 2 random rows of X as starting means; set covs = [np.eye(D)]*K, pis = np.ones(K)/K.
from sklearn.datasets import make_blobs
from sklearn.mixture import GaussianMixture
X, _ = make_blobs(200, 2, centers=2, cluster_std=0.8, random_state=0)
K, N, D = 2, *X.shape
rng = np.random.default_rng(0)
mus = X[rng.choice(N, K, replace=False)].astype(float)
covs = [np.eye(D)] * K; pis = np.ones(K) / K
for _ in range(20):
R = e_step(X, mus, covs, pis)
mus, covs, pis = m_step(X, R)
print('scratch:', np.round(mus, 3))
gm = GaussianMixture(K, random_state=0).fit(X)
print('sklearn:', gm.means_.round(3))| source | mean 0 | mean 1 |
|---|---|---|
| scratch (20 iter) | [0.964, 4.356] | [1.954, 0.762] |
| sklearn | [0.963, 4.361] | [1.952, 0.767] |
| match (<0.01) | yes | yes |
Trade off
Comparison matrix
From Milestone 3 — 20-iteration loop & validation: every row here is a choice with a cost. Fill the mean 0 column, then say which row you would actually pick and what you give up for it.
| source | mean 0 | mean 1 |
|---|---|---|
| scratch (20 iter) | [0.964, 4.356] | [1.954, 0.762] |
| sklearn | [0.963, 4.361] | [1.952, 0.767] |
| match (<0.01) | yes | yes |
Worked example
Your turn: fit a 3-component full-cov GMM on iris, call score_samples, flag the bottom-5% as anomalies. Predict how many flags you get.
Hint: np.percentile(log_probs, 5) gives the threshold; np.where(log_probs < threshold) gives anomaly indices.
from sklearn.datasets import load_iris
from sklearn.mixture import GaussianMixture
import numpy as np
X_ir = load_iris().data
gm = GaussianMixture(3, covariance_type='full', random_state=0).fit(X_ir)
lp = gm.score_samples(X_ir)
thresh = np.percentile(lp, 5)
anomalies = np.where(lp < thresh)[0]
print(f'threshold: {thresh:.3f}')
print(f'n_anomalies: {len(anomalies)}')
print(f'worst idx={lp.argmin()}, log p(x)={lp.min():.3f}')| metric | value |
|---|---|
| threshold (5th pct) | -4.398 |
| anomalies flagged | 8 of 150 (5.3%) |
| min log p(x) | -7.062 (idx 118) |
| max log p(x) | +1.624 (idx 0, normal) |
Comparison
Comparison matrix
From Milestone 4 — anomaly detection on iris: refill the value column from what you know. The rest of the table is as it appeared.
| metric | value |
|---|---|
| threshold (5th pct) | -4.398 |
| anomalies flagged | 8 of 150 (5.3%) |
| min log p(x) | -7.062 (idx 118) |
| max log p(x) | +1.624 (idx 0, normal) |
Concept
Slides closed, out loud: (1) state the GMM density formula and explain the role of pi_k; (2) describe the E-step and M-step in words without equations; (3) explain why EM can get stuck and what you do about it.
Stretch (homework): (a) derive the E-step and M-step from the expected complete-data log-likelihood E_q[log p(X,Z|theta)] and verify both update equations match what you coded; (b) compare GMM full vs k-Means on make_moons — does GMM win? (c) use BIC to select K on the iris dataset.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — The GMM density model · E-step — soft responsibilities · M-step — update parameters · GMM from scratch + applications · Your turn: build GMM from scratch. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
score_samples + percentile threshold| idea | the one thing to remember |
|---|---|
| GMM density | p(x) = sum_k pi_k N(x; mu_k, Sig_k) |
| E-step | r_ik = pi_k N(x_i) / p(x_i) — Bayes posterior |
| M-step | weighted MLE: mu_k = sum(r_ik x_i) / N_k |
| convergence | LL non-decreasing; local max only — use n_init |
| anomaly detection | score_samples(X) < low-percentile threshold |
Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.