← All tutorials

k-Means Clustering

Find the clumps.

On this page

1 k-means finds clusters without labels 2 The loop in one picture 3 Why each step lowers the same score 4 Between two centers the boundary is a straight line 5 Compact clusters work, elongated clusters break 6 Spread out the starting centers, and choose k 7 k-means in practice 8 How do you fit a model when the cluster labels are hidden? 9 References

1 k-means finds clusters without labels

Given unlabeled points, k-means sorts them into k clusters the way your eye finds clumps: guess k centers, let every point join its nearest center, move each center to the middle of its members, and repeat until the centers stop moving. This assign-move loop is Lloyd’s algorithm (Lloyd 1982).

2 The loop in one picture

There is no network, no gradient and no learning rate; the centers just walk, round by round, until they stop moving.

pick k random centers1. assigneach point joins its nearest center2. moveeach center jumps to its members' meanany center moved over 0.01?nostopyes
Figure 1. Two steps per round, repeated until the centers stop moving.

3 Why each step lowers the same score

k-means outputs k centers and one cluster per point, and both steps of the loop lower the same score.

k-means lowers one score, WCSS, the total squared distance from each point to its own center.
WCSS = Σi ‖xi − μci‖²
xi is point i. ci is the cluster it belongs to. μj is the center of cluster j, so μci is the center of point i’s own cluster. The bars ‖·‖ give a gap’s length, and WCSS, the within-cluster sum of squares, adds every point’s squared length. Lower is tighter. Squaring makes one far point cost as much as many near ones, so a stray point pulls hard on its center.
Every point joins its nearest center, then every center moves to the middle of its members, and neither step can raise WCSS.
ci = argminj ‖xi − μj‖²
Each point takes the cluster whose center is closest: argminj picks the j with the smallest squared gap. With the centers held still, that is the best cluster for every point at once, so WCSS cannot go up. Skipping the square root changes nothing about who is nearest, since the root never changes the order of distances, and it saves work over thousands of points.
μj = mean{xi : ci = j}
μj becomes the average of every point that joined cluster j this round. With the clusters held still, the average is the one spot with the smallest total squared distance to its members, so this step cannot raise WCSS either. A score that never rises and cannot drop below 0 has to settle, which is why the loop stops.

If a center loses every point, it stays put rather than dividing by zero.

The loop stops once no center moves more than 0.01 in a round.
maxj ‖μjnew − μjold‖ ≤ 0.01
The marks new and old are a center’s spot after and before this round, and maxj picks the largest move over all centers. When even the center that moved most moved 0.01 or less, another round would change almost nothing. My k-means notebooks (NumPy and PyTorch, in my Machine-Learning-Concepts repo) test the average move instead, a slightly looser rule.
1. Assign arrows point to the nearest center gray dots have no center yet C1 C2 2. Move dashed is the old spot, solid the new middle C1 C2
Figure 2. Dots take their nearest center's color; each center moves to its dots' mean.

This snippet runs the whole loop on three blobs of 100 points each and prints the three centers it settles on, near (0, 0), (4, 0) and (2, 3).

import numpy as np
rng = np.random.default_rng(0)
centers = [[0, 0], [4, 0], [2, 3]]
X = np.vstack([rng.normal(m, 0.5, size=(100, 2)) for m in centers])
k, tol, max_iter = 3, 0.01, 100
mu = X[rng.choice(len(X), k, replace=False)]
for _ in range(max_iter):
    c = ((X[:, None, :] - mu) ** 2).sum(axis=2).argmin(axis=1)
    new_mu = mu.copy()
    for j in np.unique(c):
        new_mu[j] = X[c == j].mean(axis=0)
    moved = np.linalg.norm(new_mu - mu, axis=1).max()
    mu = new_mu
    if moved <= tol:
        break
print(mu.round(2))
  1. 1Draws 300 points around three true centers and sets k, the tolerance and a cap of 100 rounds.
  2. 2Picks k distinct points as the first centers μ.
  3. 3Assigns each point to its nearest center, second eq card in Section 3, line 1.
  4. 4Moves each center to its points' mean, the same card, line 2.
  5. 5Stops when no center moved more than the tolerance, third eq card in Section 3, or after 100 rounds.

4 Between two centers the boundary is a straight line

Each point joins its nearer center, so two centers split the plane where the two distances tie.

Between two centers, the boundary is a straight line.
‖x − μ1‖²
= ‖x − μ2‖²
x is any point on the plot. The boundary is every x equally far from center μ1 and center μ2; one step to either side and the nearer center wins.
2(μ2 − μ1) · x
= ‖μ2‖² − ‖μ1‖²
Expand both sides and the x·x terms cancel, leaving a weighted sum of the features equal to a constant. That is a straight line, the same form as the line where logistic regression switches its answer. Its weights point from μ1 to μ2, so the line crosses the segment between the centers at right angles, at its midpoint.

5 Compact clusters work, elongated clusters break

A compact cluster is round, its points spread about equally in every direction; an elongated cluster is stretched much longer in one direction. k-means fits compact clusters, but the straight boundary of Section 4 passes through the midpoint between two centers, so it cuts each elongated cluster in half.

3

Figure 3. Compact clusters work; elongated clusters get cut in half.

6 Spread out the starting centers, and choose k

A random start can waste rounds or trap the loop in a lopsided split. k-means++ (Arthur and Vassilvitskii 2007) changes only the seeding: the first center is a point chosen uniformly at random, each next one is drawn like this.

In k-means++ seeding, far points are likelier picks.
P(next center = x)
= D(x)²Σx′ D(x′)²
D(x) is the distance from point x to the nearest center already picked. x′ runs over every point in the bottom sum, so the chances add up to 1. Squaring makes a point twice as far four times as likely, so the next center tends to land in a clump no center covers yet, and a point already chosen has D = 0 and is never picked again.

The centers start spread out and the loop itself never changes, so bad starts and wasted rounds get rarer.

Choosing k is up to you; two rules of thumb help: the elbow method looks for the bend in WCSS as k grows, and the silhouette score, from −1 to 1, is high when each point is much closer to its own cluster than to the next one.

This snippet fits 2 to 6 clusters on the same 300 points and prints WCSS and the silhouette score for each k; Figure 4 plots the printed values.

import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
rng = np.random.default_rng(0)
centers = [[0, 0], [4, 0], [2, 3]]
X = np.vstack([rng.normal(m, 0.5, size=(100, 2)) for m in centers])
for k in range(2, 7):
    km = KMeans(n_clusters=k, n_init=10, random_state=0)
    km.fit(X)
    print(k, km.inertia_, silhouette_score(X, km.labels_))  # WCSS, silhouette
WCSS0400800234561ksilhouette00.40.8234562k
Figure 4. 1WCSS bends at k = 3 and 2the silhouette peaks there, the 3 true blobs.

7 k-means in practice

In scikit-learn, k-means sits after a scaler, since it measures squared distance; fit runs the rounds once on the training points, and predict sends each new point to its nearest center.

import numpy as np
from sklearn.cluster import KMeans
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(0)
centers = [[0, 0], [4, 0], [2, 3]]
X = np.vstack([rng.normal(m, 0.5, size=(100, 2)) for m in centers])
model = make_pipeline(StandardScaler(), KMeans(n_clusters=3, random_state=0))
model.fit(X)
print(model.predict([[0.1, 0.2], [3.9, 0.1]]))  # nearest center
print(model[-1].n_iter_, model[-1].inertia_)  # rounds run, WCSS
  1. 1Draws the same 300 toy points.
  2. 2Chains StandardScaler with KMeans for k = 3.
  3. 3Runs assign and move rounds, the loop in Figure 2, until the centers settle.
  4. 4Gives new points their nearest center, second eq card in Section 3, line 1.
  5. 5Prints the rounds run and the WCSS, first eq card in Section 3.
stepwhen, on what, and its cost
fitOnce, in rounds; each round compares all n points with all k centers, so the average cost is k·n·T for T rounds.
predictPer new point, one distance per center; no center moves.
stateThe k centers (cluster_centers_).
settingraise it, lower itdefault
n_clusters, k in WCSSUp always lowers WCSS; k = n gives every point its own center and WCSS 0. Down merges clumps; k = 1 puts one center at the mean.8
init, the seeding card"k-means++" spreads the starting centers; "random" picks k points."k-means++"
n_initMore restarts, best WCSS kept, lower the risk of a bad split at a proportional cost."auto": 1 with k-means++, 10 with random
tol, max_iter, the stop cardSmaller tol runs more rounds; it measures the relative center change, not this page’s 0.01 move. max_iter caps the rounds.1e-4, 300

Defaults and the k·n·T cost are from the scikit-learn KMeans docs, which also warn that the result is a local minimum, hence the restarts. Where it fails is in Section 5.

8 How do you fit a model when the cluster labels are hidden?

When clusters overlap or tilt, a point deserves a share in several clusters whose labels are hidden. Maximum Likelihood and EM shows how to fit that, and Gaussian Mixture Models make it a soft k-means with tilted clusters.

9 References

  1. Lloyd, S. (1982). Least squares quantization in PCM. IEEE Transactions on Information Theory, 28(2), 129 to 137.
  2. Arthur, D., and Vassilvitskii, S. (2007). k-means++: The advantages of careful seeding. Proceedings of SODA, 1027 to 1035.
  3. Anushiravani, R. Machine-Learning-Concepts (GitHub), k-means NumPy and PyTorch notebooks; with the average-move rule they stop after 6 and 8 rounds on 5,000 noisy points from scikit-learn’s two-moons toy data.