Explain the unsupervised learning paradigm and the motivation for clustering.
Describe what a clustering algorithm produces and why there may not be one correct clustering.
Explain the K-Means algorithm at a high level and apply scikit-learn’s KMeans.
Describe important strengths and limitations of K-Means, including its sensitivity to initialization and feature representation.
Use elbow and silhouette plots to help choose the number of clusters.
Interpret clusters in applied settings such as image clustering.
Clustering looks for structure in data without using a target column. In this chapter, we will develop K-Means from its central idea, representing each group by a centre, then examine the practical choices and limitations that determine whether its clusters are useful.
Look at the foods below and consider either of the following questions before continuing.
Categorize the food items in the image and write your categories. Do you think there is one correct way to cluster these images? Why or why not?
If you want to build a machine learning model to cluster such images how would you represent such images?
Why cluster data?
The “perfect” spaghetti sauce
Suppose a company wants to make the one “perfect” spaghetti sauce. Customer preferences differ, so one recipe is unlikely to satisfy everyone. Howard Moskowitz’s market research instead identified groups of customers who preferred plain, spicy, or extra-chunky sauces. Designing products for these groups proved more useful than optimizing a single average product.
Clustering asks whether similar groups can be found in data even when no group labels are provided. The sauce story is discussed in Malcolm Gladwell’s TED talk.
What is clustering?
In supervised learning, each training example has features \(X\) and a target \(y\), and the goal is to predict the target of a new example. In unsupervised learning, we have \(X\) but no target. The goal is instead to find useful structure in the feature data.
Clustering is an unsupervised task that partitions examples into groups called clusters. A useful clustering places similar examples together and dissimilar examples apart. What counts as “similar” depends on the feature representation, distance measure, and purpose of the analysis.
Clustering input and possible output
X, y = make_blobs(n_samples=10, centers=3, n_features=2, random_state=10)fig, axes = plt.subplots(1, 2, figsize=(12, 4))discrete_scatter(X[:, 0], X[:, 1], ax = axes[0]);# user-defined plotting function defined in code/plotting_functions.pydiscrete_scatter(X[:, 0], X[:, 1], y=y, markers='o', ax = axes[1]);
The left plot below contains only the feature matrix \(X\). The colours on the right illustrate one possible assignment of each example to a cluster.
Cluster labels are arbitrary identifiers: changing labels 0, 1, and 2 to 2, 0, and 1 does not change the grouping. Unlike a supervised target, these numbers have no meaning on their own. In practice, we may not know how many clusters exist—or whether the data contains useful clusters at all.
Example 1: What is “correct” grouping?
Which of the following grouping of emoticons is the “correct” grouping?
Both groupings are reasonable: one uses facial expression and the other uses whether the emoticon includes a nose.
There is therefore no universally correct grouping. Domain knowledge and the intended use determine whether a clustering is meaningful, which also makes clustering quality difficult to measure with a single number.
Why clustering can be useful
Clustering can summarize a dataset, reveal groups for further investigation, or partition examples before another analysis. For example, after fitting a supervised model, we might inspect its performance separately on discovered clusters to find a group on which it performs poorly. The clusters still require interpretation; the algorithm does not explain what they mean.
Customer segmentation
Understand landscape of the market in businesses and craft targeted business or marketing strategies tailored for each group.
Grouping articles on different topics from different news sources. For example, Google News.
You’ll be working on document clustering, image clustering, and multimodal clustering in the lab.
Other applications include social-network analysis, image segmentation, anomaly detection, and data compression.
K-Means clustering
K-Means defines similarity using Euclidean distance, just as \(k\)-nearest neighbours does. It represents each cluster by a centre and assigns nearby examples to that centre.
K-Means takes two inputs:
X\(\rightarrow\) a set of data points
K (or \(k\) or n_clusters) \(\rightarrow\) number of clusters
It returns one of K cluster labels for each example and learns K cluster centres.
We will begin with the toy dataset above, where three compact groups are visually apparent.
X, y = make_blobs(n_samples=10, centers=3, n_features=2, random_state=10)discrete_scatter(X[:, 0], X[:, 1]);
We set n_clusters=3 and pass only X to fit; clustering does not use target labels.
from sklearn.cluster import KMeanskmeans = KMeans(n_clusters=3, n_init='auto')kmeans.fit(X);# We are only passing X because this is unsupervised learning
predict returns the cluster assignment for each example.
The stars mark the centroids, or means of the examples assigned to each cluster. A centroid lies in the feature space but is not usually an observed example.
A fitted K-Means model can also assign new examples to their nearest learned centroid.
Consider the two new query points shown with triangles below.
If the centres were known, we could assign each example to its nearest centre. If the assignments were known, we could compute each centre by taking a mean. K-Means resolves this circular dependency by alternating between these two operations.
Starting from \(K\) initial centres, the algorithm repeats two steps:
Assignment: assign every example to its nearest centre.
Update: replace each centre with the mean of the examples assigned to it.
It stops when the centres no longer change enough or when it reaches the maximum number of iterations.
A worked K-Means example
Let’s execute K-Means algorithm on our toy example.
Input - The data points X
n_examples = X.shape[0]print("Number of examples: ", n_examples)X
Random initialization for K initial centers of the clusters.
np.random.seed(seed=3)centers_idx = np.random.choice(range(0, n_examples), size=k)centers = X[centers_idx]plot_km_initialization(X, centers) # user-defined plotting function defined in code/plotting_functions.py
Iterative process
repeat
Assign each example to the closest center. (update_Z)
Estimate new centers as average of observations in a cluster. (update_centers)
until centers stop changing or maximum iterations have reached.
First step in the iterative process is assigning examples to the closest center. How to find closest centers?
Let’s consider distance of an example to all centers and assign that example to the closest center.
import panel as pnfrom panel import widgetsfrom panel.interact import interactimport matplotlib.pyplot as pltpn.extension()def f(point_index): fig = plt.figure(figsize=(6, 4))# user-defined plotting function defined in code/plotting_functions.pyreturn plot_example_dist(X, centers, fig, point_ind=point_index)interact(f, point_index=widgets.FloatSlider(start=0, end=9, step=1, value=0)).embed(max_opts=9)# interact(f, point_index=widgets.FloatSlider(start=0, end=9, step=1, value=0))
Similarly, we can make cluster assignments for all points by calculating distances of all examples to the centers and assigning it to the cluster with smallest distance.
from sklearn.metrics import euclidean_distancesdef update_Z(X, centers):""" returns distances and updated cluster assignments """ dist = euclidean_distances(X, centers)return dist, np.argmin(dist, axis=1)
With the new cluster assignments for our data points, we update cluster centers. How do we update centers?
New cluster centers are means of data points in each cluster.
Let’s put these steps together. - Initialize - Iteratively alternate between the following two steps. - Update assignments\(Z \rightarrow\) Assign each example to the closest center - Update centers\(\rightarrow\) Estimate new centers as average of examples in a cluster
Let’s examine the initial centers.
plot_km_initialization(X, centers)
Here is the path cluster centers took and their cluster assignements in six iterations.
plot_km_iterative(X, X[centers_idx], 6) # user-defined plotting function defined in code/plotting_functions.py
Initialization matters
K-Means can converge to different solutions from different initial centres. The next example shows how a poor initialization can produce a worse grouping.
Scikit-learn uses K-Means++ by default, which spreads the initial centres apart. The n_init parameter controls how many initializations are tried; KMeans retains the run with the lowest within-cluster sum of squares. A fixed random_state makes the result reproducible.
When K-Means works well
K-Means is simple and usually efficient, but its Euclidean-distance objective makes several implicit assumptions. It works best when clusters are compact, roughly spherical, similarly sized, and reasonably well separated.
Feature scale matters because a feature with a large numeric range can dominate the distance calculation. As with \(k\)-nearest neighbours, numeric features should usually be scaled before fitting K-Means. Feature representation matters just as much: clustering images by raw pixel values, for example, can produce very different groups than clustering representations that encode visual content.
K-Means is also sensitive to outliers. Because each centroid is a mean, an extreme example can pull its centroid away from the rest of the cluster. Finally, every example receives exactly one cluster label, even when it lies between groups or does not belong naturally to any cluster.
Optional details about K-Means
(Optional) Objective function for K-Means
Find the local minimum of minimizing squared distances (L2 norm).
The algorithm optimizes the sum of the distances of the cluster centers to all the points in that cluster. In other words, it minimizes within-cluster sum-of-squares criterion.
Naive implementation of K-Means requires you to compute the distances from all data points to all cluster centers.
So there are many distance calculations per iteration.
calculating assigning observations to centers is heavy: \(\mathcal{O(ndk)}\)
updating centers is light(er): \(\mathcal{O(nd)}\)
where,
\(n \rightarrow\) number of examples
\(d \rightarrow\) number of features
\(k \rightarrow\) number of clusters
There are more efficient exact algorithms.
Elkan’s (implemented in scikit-learn)
Ying-Yang
Here, the meaning of exact is that they give you exactly the same result as the Lloyd’s algorithm but do that more efficiently.
Also, other approximate algorithms are being developed.
Exercises
Exercise 1
Select all statements that are true.
K-Means algorithm always converges to the same solution.
\(K\) in K-Means should always be \(\leq\) # of features.
In K-Means, it makes sense to have \(K\)\(\leq\) # of examples.
In K-Means, in some iterations some points may be left unassigned.
TipSolution
C
Exercise 2
Select all statements that are true.
K-Means is sensitive to initialization and the solution may change depending upon the initialization.
K-means terminates when the number of clusters does not increase between iterations.
K-means terminates when the centroid locations do not change between iterations.
K-Means is guaranteed to find the optimal solution.
TipSolution
A, C
Choosing the number of clusters
K-Means requires n_clusters in advance. Without target labels, we cannot select \(K\) using supervised validation scores. Domain knowledge should guide the choice whenever possible; elbow and silhouette plots provide additional evidence, not an automatic answer.
The elbow method
Inertia is the sum of squared distances from each example to its assigned centroid. For three clusters,
\(C_1,C_2,C_3\) are the centroids and each \(P_i\) is an example assigned to that cluster.
Scikit-learn stores inertia in the fitted model’s inertia_ attribute.
XX, y = make_blobs(centers=3, n_features=2, random_state=10)discrete_scatter(XX[:, 0], XX[:, 1], markers="o");
d = {"K": [], "inertia": []}for k inrange(1, 100, 10): model = KMeans(n_clusters=k, n_init='auto').fit(XX) d["K"].append(k) d["inertia"].append(model.inertia_)
pd.DataFrame(d)
K
inertia
0
1
4372.460950
1
11
70.076284
2
21
26.137410
3
31
15.359887
4
41
6.844921
5
51
3.593508
6
61
2.126488
7
71
1.016676
8
81
0.385639
9
91
0.053156
Inertia always decreases as \(K\) increases; with one cluster per example, it reaches zero. The elbow method therefore looks for a bend after which additional clusters yield only small reductions in inertia. It balances compact clusters against a simpler clustering with fewer groups.
inertia_values =list()for k inrange(1, 10): inertia_values.append(KMeans(n_clusters=k, n_init='auto').fit(XX).inertia_)plot_elbow(6, 4, inertia_values)
Here, \(K=3\) is a reasonable choice: beyond three clusters, the improvement is comparatively small. Real elbow plots are often ambiguous.
The yellowbrick package provides a convenient elbow visualizer.
from yellowbrick.cluster import KElbowVisualizermodel = KMeans()visualizer = KElbowVisualizer(model, k=(1, 10))visualizer.fit(XX) # Fit the data to the visualizervisualizer.show();
The silhouette method
The silhouette score compares how close an example is to its own cluster with how close it is to the nearest alternative cluster. It can be used with clustering methods that do not define centroids.
Mean intra-cluster distance (\(a\))
Consider the green point below.
The mean intra-cluster distance for the point is the average of the distances of the green point to the other points in the same cluster.
These distances are represented by the black lines in the plot below.
plot_silhouette_dist(6, 4)
Mean nearest-cluster distance (\(b\))
Average of the distances of the green point to the blue points is smaller than the average of the distances of the green point to the red points. So the nearest cluster is the blue cluster.
So, the mean nearest-cluster distance is the average of the distances of the green point to the blue points.
Silhouette distance for a data point
The silhouette distance for a data point the difference between the the average nearest-cluster distance (\(b\)) and average intra-cluster distance (\(a\)) for each data point, normalized by the maximum value
\[\frac{b-a}{max(a,b)}\]
The best value is 1.
The worst value is -1 (samples have been assigned to wrong clusters).
Value near 0 means overlapping clusters i.e., the example is on or very close to the decision boundary between two neighbouring clusters.
The overall Silhouette score is the average of the Silhouette scores for all examples. We can visualize the silhouette score for each example individually in a silhouette plot (hence the name), see below.
Using Silhouette scores to select the number of clusters
The plots below show the Silhouette scores for each sample in that cluster.
from yellowbrick.cluster import SilhouetteVisualizer
model = KMeans(2, n_init='auto', random_state=42)visualizer = SilhouetteVisualizer(model, colors="yellowbrick")visualizer.fit(XX) # Fit the data to the visualizervisualizer.show();# Finalize and render the figure
model = KMeans(5, n_init='auto', random_state=42)visualizer = SilhouetteVisualizer(model, colors="yellowbrick")visualizer.fit(XX) # Fit the data to the visualizervisualizer.show();# Finalize and render the figure
model = KMeans(3, n_init='auto', random_state=42)visualizer = SilhouetteVisualizer(model, colors="yellowbrick")visualizer.fit(XX) # Fit the data to the visualizervisualizer.show();# Finalize and render the figure
What to look for in these plots?
The thickness of each silhouette represents the size of that cluster. In the above plot, our three clusters are of similar sizes.
The length (or area) of each silhouette indicates the “goodness” of each cluster.
A slower dropoff (more rectangular) indicates more points are “happy” in their cluster.
The red dashed line shows the average silhouette score for all samples, which tells you the overall clustering fit. The close this score is to 1, the better the clustering fit is. In our example, the average score seems to be around 0.78, which suggests a strong cluster structure.
For a well-fitted clustering model, you’d expect to see the silhouette plots for each cluster above the average silhouette score line, and with widths which do not vary wildly.
In general, if any cluster has many points below the average silhouette score, this could be a sign that the cluster is not well separated from its neighbouring cluster, or it has too much internal variance, suggesting that the number of cluster chosen might not be ideal.
We can apply Silhouette method to clustering methods other than K-Means.
Limitations
Neither measure establishes a true number of clusters. Elbows can be subjective, and both measures tend to favour compact, well-separated groups. They may be misleading for clusters with complex shapes, unequal densities, or application-specific meanings. Treat these plots as diagnostics and combine them with domain knowledge and qualitative inspection of the resulting clusters.
Exercises
Exercise 3
Select all statements that are true.
If you train K-Means with n_clusters= the number of examples, the inertia value will be 0.
The elbow plot shows the tradeoff between within cluster distance and the number of clusters.
Unlike the Elbow method, the Silhouette method is not dependent on the notion of cluster centers.
The elbow plot is not a reliable method to obtain the optimal number of clusters in all cases.
The Silhouette scores ranges between -1 and 1 where higher scores indicates better cluster assignments.
TipSolution
A, B, C, D, E
NoteOptional: Gaussian mixture models
Gaussian mixture models (GMMs) provide a more flexible, probabilistic alternative to K-Means. They are not part of the core material for this chapter, but this section is available for readers who want to see how soft cluster assignments and non-spherical clusters can be handled.
Motivation
K-Means represents every cluster only by a centroid, so it works best for compact, roughly spherical groups. Consider data containing elongated groups instead.
km = KMeans(n_clusters=3, n_init="auto")km.fit(X_train)
KMeans(n_clusters=3)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_clusters
3
init
'k-means++'
n_init
'auto'
max_iter
300
tol
0.0001
verbose
0
random_state
None
copy_x
True
algorithm
'lloyd'
km_labels = km.labels_
plot_kmeans_circles(km, X_train, n_clusters=3)
K-Means divides the space according to distance from the centroids and cannot capture these elongated groups well. A Gaussian mixture model (GMM) also models each component’s spread and orientation through a covariance matrix.
from sklearn.mixture import GaussianMixturegmm = GaussianMixture(n_components=3, covariance_type="full") # more on covariance_type in a bit gmm.fit(X_train)gmm_labels = gmm.predict(X_train)
Because our dataset is two-dimensional, we get a 2 by 2 covariance matrix for each cluster.
There is a non-negative weight associated with each component which represents the proportion of total distribution contributed by that component.
gmm.weights_
array([0.14042383, 0.28682168, 0.57275449])
We have trained our model with three components so we have three weights (prior probabilities) associated with each component. The sum of the weights is 1.0.
np.sum(gmm.weights_)
np.float64(1.0)
How does a GMM work?
Given \(K\), the number of clusters or the number of components, we want to fit Gaussian blobs to the data. Each Gaussian will have its own mean vector and covariance matrix.
This is a generative model; it models the probability of a given data point being generated from the mixture of the Gaussians.
\(\pi_k \rightarrow\) the weight of component \(k\), where \(0 \leq \pi_k \leq 1\) and \(\sum_{k=1}^{K} \pi_k = 1\)
\(K \rightarrow\) the number of clusters or the number of components
\(k \rightarrow\) index of a component, where \(k \in \{1, 2, \dots, K\}\)
\(\mu_k \rightarrow\) the mean vector of component \(k\)
\(\Sigma_k \rightarrow\) the covariance matrix associated with component \(k\)
The generative story of the model assumes that each data point in the dataset is generated from one of the Gaussian components. So for each example \(x\): - Choose component \(k\) with probabilities proportional to the weight \(\pi_k\) (prior probability) of the components.
Choose example \(x\) from the Gaussian distribution associated with the \(k^{th}\) component: \(\mathcal{N}(x \mid \mu_k, \Sigma_k)\)
How to interpret the weights associated with the Gaussians? Let’s look at a toy example with of a mixture of Gaussians, i.e., a weighted sum of Gaussians.
A mixture of Gaussians can model much more complicated shapes than a single Gaussian distribution.
The goal is to estimate \(\pi_k\), \(\mu_k\), \(\Sigma_k\) for all clusters or components \(k\). - It’s a non-convex optimization problem
It is sensitive to initialization. Usually, it’s initialized with K-Means.
Generally used with “soft” assignments. Each point contributes to the mean and covariance of each component but the points that are far away only contribute a little.
Under the hood it finds these parameters using an algorithm called Expectation Maximization. The idea is to treat the clusters as hidden variables.
Choose starting guesses for the location and shape
Repeat until converged:
E-step: for each point, find weights encoding the probability of membership in each cluster
M-step: for each cluster, update its location, normalization, and shape based on all data points, making use of the weights
We can constrain the covariance of the Gaussians using the covariance_type option when creating a GaussianMixture object. This allows us to control the shape and complexity of clusters.
Since GMM is a generative model, we can get the log likelihood of the model generating this data.
estimators['full'].score(X_train)
np.float64(-5.073780282241251)
It’s possible to do model selection, i.e., selecting the appropriate covariance type and the number of components based on Akaike Information Criterion (AIC) or Bayesian Information Criterion (BIC) which penalize complex models.
n_components =range(1,15)gmm_models = [GaussianMixture(n_components=k).fit(X_train) for k in n_components]aic_scores = [model.aic(X_train) for model in gmm_models]bic_scores = [model.bic(X_train) for model in gmm_models]data = np.vstack([n_components, aic_scores, bic_scores]).Tpd.DataFrame(data, columns=['n_components', 'aic', 'bic'])np.argmin(bic_scores)plt.plot(n_components, bic_scores, label='BIC')plt.plot(n_components, aic_scores, label='AIC')plt.legend(loc='best')plt.xlabel('n_components');
Here, both AIC and BIC are smallest for n_components=3. See an example of using grid search to select the number of components using BIC here.
Optional exercise
Select all statements that are true.
GMMs are more flexible than KMeans but can be computationally expensive.
In GMMs, each data point has a probability associated with each component.
GMMs are sensitive to the initialization.
The number of components in a GMM has no effect on the model’s ability to fit the data.
TipSolution
A, B, C
Summary
No targets are needed. Clustering looks for structure in \(X\) without using a target \(y\).
Assign, average, repeat. K-Means assigns each example to its nearest centroid, recomputes the centroids as means, and repeats until they stabilize.
Cluster labels are just names. Labels such as 0, 1, and 2 identify groups but have no numerical meaning.
Representation defines similarity. Feature choice and scaling determine which examples are close under Euclidean distance.
K-Means has a preferred cluster shape. It works best for compact, roughly spherical, similarly sized clusters and can be strongly affected by outliers.
Initialization matters. K-Means can converge to a suboptimal solution, so K-Means++ and multiple initializations are useful.
There is rarely one correct \(K\). Elbow and silhouette plots provide evidence, but useful clusters must also be interpretable and meaningful for the application.