348 KiB
348 KiB
In [1]:
%matplotlib inline
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
from IPython.display import display
np.random.seed(2021)In [2]:
def gaussian_points(dim=2, n_points=1000, mean_vector=np.array([0, 0]),
sample_variance=1):
"""
Very simple custom function to generate gaussian distributed point clusters
with variable dimension, number of points, means in each direction
(must match dim) and sample variance.
Inputs:
dim (int)
n_points (int)
mean_vector (np.array) (where index 0 is x, index 1 is y etc.)
sample_variance (float)
Returns:
data (np.array): with dimensions (dim x n_points)
"""
mean_matrix = np.zeros(dim) + mean_vector
covariance_matrix = np.eye(dim) * sample_variance
data = np.random.multivariate_normal(mean_matrix, covariance_matrix,
n_points)
return data
def generate_simple_clustering_dataset(dim=2, n_points=1000, plotting=True,
return_data=True):
"""
Toy model to illustrate k-means clustering
"""
data1 = gaussian_points(mean_vector=np.array([5, 5]))
data2 = gaussian_points()
data3 = gaussian_points(mean_vector=np.array([1, 4.5]))
data4 = gaussian_points(mean_vector=np.array([5, 1]))
data = np.concatenate((data1, data2, data3, data4), axis=0)
if plotting:
fig, ax = plt.subplots()
ax.scatter(data[:, 0], data[:, 1], alpha=0.2)
ax.set_title('Toy Model Dataset')
plt.show()
if return_data:
return data
data = generate_simple_clustering_dataset()In [3]:
n_samples, dimensions = data.shape
n_clusters = 4
# we randomly initialize our centroids
np.random.seed(2021)
centroids = data[np.random.choice(n_samples, n_clusters, replace=False), :]
distances = np.zeros((n_samples, n_clusters))
# first we need to calculate the distance to each centroid from our data
for k in range(n_clusters):
for n in range(n_samples):
dist = 0
for d in range(dimensions):
dist += np.abs(data[n, d] - centroids[k, d])**2
distances[n, k] = dist
# we initialize an array to keep track of to which cluster each point belongs
# the way we set it up here the index tracks which point and the value which
# cluster the point belongs to
cluster_labels = np.zeros(n_samples, dtype='int')
# next we loop through our samples and for every point assign it to the cluster
# to which it has the smallest distance to
for n in range(n_samples):
# tracking variables (all of this is basically just an argmin)
smallest = 1e10
smallest_row_index = 1e10
for k in range(n_clusters):
if distances[n, k] < smallest:
smallest = distances[n, k]
smallest_row_index = k
cluster_labels[n] = smallest_row_indexIn [4]:
fig = plt.figure()
ax = fig.add_subplot()
unique_cluster_labels = np.unique(cluster_labels)
for i in unique_cluster_labels:
ax.scatter(data[cluster_labels == i, 0],
data[cluster_labels == i, 1],
label = i,
alpha = 0.2)
ax.scatter(centroids[:, 0], centroids[:, 1], c='black')
ax.set_title("First Grouping of Points to Centroids")
plt.show()In [5]:
max_iterations = 100
tolerance = 1e-8
start_time = time.time()
for iteration in range(max_iterations):
prev_centroids = centroids.copy()
for k in range(n_clusters):
# this array will be used to update our centroid positions
vector_mean = np.zeros(dimensions)
mean_divisor = 0
for n in range(n_samples):
if cluster_labels[n] == k:
vector_mean += data[n, :]
mean_divisor += 1
# update according to the k means
centroids[k, :] = vector_mean / mean_divisor
# we find the dissimilarity
for k in range(n_clusters):
for n in range(n_samples):
dist = 0
for d in range(dimensions):
dist += np.abs(data[n, d] - centroids[k, d])**2
distances[n, k] = dist
# assign each point
for n in range(n_samples):
smallest = 1e10
smallest_row_index = 1e10
for k in range(n_clusters):
if distances[n, k] < smallest:
smallest = distances[n, k]
smallest_row_index = k
cluster_labels[n] = smallest_row_index
# convergence criteria
centroid_difference = np.sum(np.abs(centroids - prev_centroids))
if centroid_difference < tolerance:
print(f'Converged at iteration {iteration}')
print(f'Runtime: {time.time() - start_time} seconds')
break
elif iteration == max_iterations:
print(f'Did not converge in {max_iterations} iterations')
print(f'Runtime: {time.time() - start_time} seconds')Converged at iteration 5 Runtime: 0.23237395286560059 seconds
In [6]:
fig = plt.figure()
ax = fig.add_subplot()
unique_cluster_labels = np.unique(cluster_labels)
for i in unique_cluster_labels:
ax.scatter(data[cluster_labels == i, 0],
data[cluster_labels == i, 1],
label = i,
alpha = 0.2)
ax.scatter(centroids[:, 0], centroids[:, 1], c='black')
ax.set_title("Final Result of K-means Clustering")
plt.show()In [7]:
def get_distances_to_clusters(data, centroids):
"""
Function that for each cluster finds the squared Euclidean distance
from every data point to the cluster centroid and returns a numpy array
containing the distances such that distance[i, j] means the distance between
the i-th point and the j-th centroid.
Inputs:
data (np.array): with dimensions (n_samples x dim)
centroids (np.array): with dimensions (n_clusters x dim)
Returns:
distances (np.array): with dimensions (n_samples x n_clusters)
"""
n_samples, dimensions = data.shape
n_clusters = centroids.shape[0]
distances = np.zeros((n_samples, n_clusters))
for k in range(n_clusters):
for i in range(n_samples):
dist = 0
for j in range(dimensions):
dist += np.abs(data[i, j] - centroids[k, j])**2
distances[i, k] = dist
return distances
def assign_points_to_clusters(distances):
"""
Function to assign each data point to the cluster to which it is the closest
based on the squared Euclidean distance from the get_distances_to_clusters
method.
Inputs:
distances (np.array): with dimensions (n_samples x n_clusters)
Returns:
cluster_labels (np.array): with dimensions (n_samples)
"""
cluster_labels = np.argmin(distances, axis=1)
return cluster_labels
def k_means(data, n_clusters=4, max_iterations=100, tolerance=1e-8):
"""
Naive implementation of the k-means clustering algorithm. A short summary of
the algorithm is as follows: we randomly initialize k centroids / means.
Then we assign, using the squared Euclidean distance, every data-point to a
cluster. We then update the position of the k centroids / means, and repeat
until convergence or we reach our desired maximum iterations. The method
returns the cluster assignments of our data-points and a sequence of
centroids.
Inputs:
data (np.array): with dimesions (n_samples x dim)
n_clusters (int): hyperparameter which depends on dataset
max_iterations (int): hyperparameter which depends on dataset
tolerance (float): convergence measure
Returns:
cluster_labels (np.array): with dimension (n_samples)
centroid_list (list): list of centroids (np.array)
with dimensions (n_clusters x dim)
"""
samples, dimensions = data.shape
np.random.seed(2021)
centroids = data[np.random.choice(len(data), n_clusters, replace=False), :]
distances = get_distances_to_clusters(data, centroids)
cluster_labels = assign_points_to_clusters(distances)
start_time = time.time()
for iteration in range(max_iterations):
prev_centroids = centroids.copy()
for k in range(n_clusters):
vector_mean = np.zeros(dimensions)
mean_divisor = 0
for n in range(n_samples):
if cluster_labels[n] == k:
vector_mean += data[n, :]
mean_divisor += 1
# And update according to the new means
centroids[k, :] = vector_mean / mean_divisor
distances = get_distances_to_clusters(data, centroids)
cluster_labels = assign_points_to_clusters(distances)
centroid_difference = np.sum(np.abs(centroids - prev_centroids))
if centroid_difference < tolerance:
print(f'Converged at iteration: {iteration}')
print(f'Runtime: {time.time() - start_time} seconds')
return cluster_labels, centroids
print(f'Did not converge in {max_iterations} iterations')
print(f'Runtime: {time.time() - start_time} seconds')
return cluster_labels, centroids
# quirk of numpy / Jupyter need to set seed again
cluster_labels, centroids = k_means(data)Converged at iteration: 5 Runtime: 0.19273090362548828 seconds
In [8]:
test_data = generate_simple_clustering_dataset(n_points=10000, plotting=False)
%prun -l 10 cluster_labels, centroids = k_means(test_data)Converged at iteration: 11 Runtime: 0.386091947555542 seconds
In [9]:
def np_get_distances_to_clusters(data, centroids):
"""
Squared Euclidean distance between all data-points and every centroid. For
the function to work properly it needs data and centroids to be numpy
broadcastable. We sum along the dimension axis.
Inputs:
data (np.array): with dimensions (samples x 1 x dim)
centroids (np.array): with dimensions (1 x n_clusters x dim)
Returns:
distances (np.array): with dimensions (samples x n_clusters)
"""
distances = np.sum(np.abs((data - centroids))**2, axis=2)
return distances
def np_assign_points_to_clusters(distances):
"""
Assigning each data-point to a cluster given an array distances containing
the squared Euclidean distance from every point to each centroid. We do
np.argmin along the cluster axis to find the closest cluster. Returns a
numpy array with corresponding labels.
Inputs:
distances (np.array): with dimensions (samples x n_clusters)
Returns:
cluster_labels (np.array): with dimensions (samples x None)
"""
cluster_labels = np.argmin(distances, axis=1)
return cluster_labels
def np_k_means(data, n_clusters=4, max_iterations=100, tolerance=1e-8):
"""
Numpythonic implementation of the k-means clusting algorithm.
Inputs:
data (np.array): with dimesions (samples x dim)
n_clusters (int): hyperparameter which depends on dataset
max_iterations (int): hyperparameter which depends on dataset
tolerance (float): convergence measure
progression_plot (bool): activation flag for plotting
Returns:
cluster_labels (np.array): with dimension (samples)
centroid_list (list): list of centroids (np.array)
with dimensions (n_clusters x dim)
"""
n_samples, dimensions = data.shape
np.random.seed(2021)
centroids = data[np.random.choice(len(data), n_clusters, replace=False), :]
distances = np_get_distances_to_clusters(np.reshape(data,
(n_samples, 1, dimensions)),
np.reshape(centroids,
(1, n_clusters, dimensions)))
cluster_labels = np_assign_points_to_clusters(distances)
start_time = time.time()
for iteration in range(max_iterations):
prev_centroids = centroids.copy()
for k in range(n_clusters):
points_in_cluster = data[cluster_labels == k]
mean_vector = np.mean(points_in_cluster, axis=0)
centroids[k] = mean_vector
distances = np_get_distances_to_clusters(np.reshape(data,
(n_samples, 1, dimensions)),
np.reshape(centroids,
(1, n_clusters, dimensions)))
cluster_labels = np_assign_points_to_clusters(distances)
centroid_difference = np.sum(np.abs(centroids - prev_centroids))
if centroid_difference < tolerance:
print(f'Converged at iteration: {iteration}')
print(f'Runtime: {time.time() - start_time} seconds')
return cluster_labels, centroids
print(f'Did not converge in {max_iterations} iterations')
print(f'Runtime: {time.time() - start_time} seconds')
return cluster_labels, centroidsIn [10]:
cluster_labels, centroids = np_k_means(data)Converged at iteration: 5 Runtime: 0.002377033233642578 seconds
In [ ]: