You may wonder, "Why can the initial centroid placement result in different clustering results?" Well, the K-means algorithm is an iterative procedure that minimizes the within-cluster sum of squares. However, it only guarantees finding a local minimum, not a global one. This implies that different starting positions can lead to distinct clustering outcomes.
To visualize this, imagine you're blindfolded in a hilly region where you're tasked to find the lowest point. By feeling the ground slope, you move downwards. But when there are many valleys (local minima), your starting position influences which valley (local minimum) you'll end up in - and not all valleys are equally deep. Initial centroids in K-means are akin to starting positions.
Fortunately, real-world applications rarely suffer from the infamous K-means' local minima issue. Plus, Python libraries such as scikit-learn go a long way in handling these concerns proficiently. In particular, scikit's KMeans function uses an intelligent initialization technique called "K-Means++" by default. This approach systematically finds a good set of initial centroids, reducing the likelihood of poor clustering due to unlucky centroid initialization. It's worth mentioning that creating an example that demonstrates sensitivity to initial centroid location is not straightforward due to KMeans' clever centroid initialization.
Nonetheless, it’s still good to be aware of the importance of initial centroids, as in more intricate clustering methods, centroid initialization may significantly impact results. We can illustrate this using a custom implementation of KMeans, since it's very basic and, therefore, it's very sensitive to the choice of initial centroids.
To do that, let's first prepare the data for our illustration:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
np.random.seed(0)
# Data preparation
x1 = np.random.normal(loc=5, scale=1, size=(100, 2))
x2 = np.random.normal(loc=10, scale=2, size=(100, 2))
x = np.concatenate([x1, x2])
Now, let's revisit our custom implementation from the beginning of this lesson:
# K-means clustering preparation
k = 3
def calc_distance(x1, x2):
return (sum((x1 - x2)**2))**0.5
def find_closest_centroids(centroids, x):
assigned_centroid = []
for i in x:
distance = []
for j in centroids:
distance.append(calc_distance(i, j))
assigned_centroid.append(np.argmin(distance))
return assigned_centroid
def calc_centroids(clusters, x):
new_centroids = []
new_df = pd.concat([pd.DataFrame(x), pd.DataFrame(clusters, columns=['cluster'])], axis=1)
for c in set(new_df['cluster']):
current_cluster = new_df[new_df['cluster'] == c][new_df.columns[:-1]]
cluster_mean = current_cluster.mean(axis=0)
new_centroids.append(cluster_mean)
return new_centroids
# K-means clustering function
def kmeans_clustering(x, initial_centroids):
centroids = initial_centroids
for i in range(10):
get_centroids = find_closest_centroids(centroids, x)
centroids = calc_centroids(get_centroids, x)
return centroids, get_centroids
The above code is the K-Means Clustering implementation, which we used in the previous units with a small difference — we now pass initial centroids as a parameter to our kmeans_clustering function. Now, let's perform clustering with different initial centroids:
# Initial centroids for two different initializations
np.random.seed(42) # Keeping the seed constant for reproducibility
initial_centroids1 = x[np.random.choice(range(x.shape[0]), size=k, replace=False), :]
initial_centroids2 = x[np.random.choice(range(x.shape[0]), size=k, replace=False), :]
# Applying K-means clustering with different initial centroids will affect the final clustering
centroids1, get_centroids1 = kmeans_clustering(x, initial_centroids1)
centroids2, get_centroids2 = kmeans_clustering(x, initial_centroids2)
# Visualization of the final clustering for both sets of initial centroids
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
axs[0].scatter(x[:,0], x[:,1], c=get_centroids1)
axs[0].scatter(np.array(centroids1)[:,0], np.array(centroids1)[:,1], c='red', marker='X')
axs[0].set_title("First Initialization")
axs[1].scatter(x[:,0], x[:,1], c=get_centroids2)
axs[1].scatter(np.array(centroids2)[:,0], np.array(centroids2)[:,1], c='red', marker='X')
axs[1].set_title("Second Initialization")
plt.show()
The visualization below illustrates the significance of initial centroids:
