Как вы все уже знаете, поскольку вы попали на эту страницу, кластеризация — это фундаментальный метод машинного обучения и анализа данных. Это полезно для нескольких целей, таких как классификация, сегментация, уменьшение размерности, обнаружение выбросов и извлечение дополнительных сведений из данных. Алгоритмы неконтролируемой кластеризации, такие как K-means и DBSCAN, не требуют предварительных знаний или обучения и могут идентифицировать кластеры для группировки точек данных, которые находятся ближе друг к другу. В Интернете есть несколько хороших руководств по K-means и DBSCAN. В этой статье представлен краткий обзор двух алгоритмов, K-средних и DBSCAN, и показаны простые реализации для одномерного случая.

Начнем с импорта обычных пакетов.

import matplotlib.pyplot as plt
from scipy import signal
import numpy as np

Кластеры данных могут быть созданы путем создания различных эллипсов и добавления шума. Мы определяем следующий метод для генерации всех данных для каждого кластера.

def generate_ellipse(npts,majaxis,minaxis,rotation,center,nvar):
    # Generate angles between 0 and 2*pi
    angles = np.linspace(0, 2 * np.pi, npts)
    # Calculate x and y points on ellipse
    x1 = center[0] + majaxis*np.cos(angles)*np.cos(rotation) - minaxis*np.sin(angles)*np.sin(rotation)
    y1 = center[1] + majaxis*np.cos(angles)*np.sin(rotation) + minaxis*np.sin(angles)*np.cos(rotation)
    noise = np.random.normal(0, nvar, size=x2.shape)
    x1 += noise
    noise = np.random.normal(0, nvar, size=y2.shape)
    y1 += noise
    ellipsoid = np.column_stack((x1, y1))
    return ellipsoid

Затем с помощью следующего кода мы генерируем данные, имеющие два кластера.

ellipse1 = generate_ellipse(1000,1,0.5,np.pi/4,(5,2),0.5)
ellipse2 = generate_ellipse(1000,1,0.5,np.pi/4,(1,1),0.5)
data = np.concatenate((ellipse1, ellipse2))
plt.figure()
plt.scatter(data[:, 0], data[:, 1])
plt.title('Data')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

Кластеризация K-средних находит центроиды, которые представляют каждый из кластеров, найденных в данных. Однако нам нужно указать количество кластеров, которые есть в наборе данных. Алгоритм начинается со случайных центроидов для определенного количества кластеров. Для каждого центроида мы находим все точки данных, которые ближе к этому центроиду по сравнению с другими центроидами. Этот шаг также можно заменить вычислением вероятности того, что точки данных, принадлежащие кластеру, определяются этим центроидом. Как только подмножество точек данных связано с этим кластером, центроид затем повторно вычисляется на основе всех точек данных, связанных с этим кластером. Это составляет одну итерацию алгоритма. Итерации продолжаются до тех пор, пока центроиды не перестанут меняться.

from sklearn.cluster import KMeans
# Fit a k-means model to the data
kmeans = KMeans(n_clusters=2)
kmeans.fit(data)

# Get the cluster labels and centroids
labels = kmeans.labels_
centroids = kmeans.cluster_centers_

# Plot the data points and centroids
plt.figure()
plt.scatter(data[:, 0], data[:, 1], c=labels)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='x', s=200, linewidths=3, color='r')
plt.title('K-means Clustering')
plt.show()

Если мы изменим количество кластеров на другое число, то увидим, что кластеризация K-средних группирует точки данных в эти многочисленные кластеры. На практике мы не знаем, сколько кластеров содержится в данных, и нам нужно применять дополнительные стратегии для оценки количества кластеров.

kmeans = KMeans(n_clusters=5)
kmeans.fit(data)

# Get the cluster labels and centroids
labels = kmeans.labels_
centroids = kmeans.cluster_centers_

# Plot the data points and centroids
plt.figure()
plt.scatter(data[:, 0], data[:, 1], c=labels)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='x', s=200, linewidths=3, color='r')
plt.title('K-means Clustering')
plt.show()

DBSCAN, с другой стороны, использует шар эпсилон для группировки точек данных вместе. Мы рисуем эпсилон-шар вокруг точки данных, и если в радиусе эпсилон есть хотя бы определенное количество соседей, мы связываем их с точкой данных. Затем это повторяется для каждого из соседей. Все точки данных, до которых можно добраться друг от друга (то есть все они находятся в пределах эпсилон-расстояния друг от друга), группируются вместе в один кластер. Те точки данных, которые не имеют достаточного количества соседей в пределах своего эпсилон-шара, рассматриваются как выбросы.

На следующем графике показано, как DBSCAN группирует точки данных. Здесь мы видим, что мы по-прежнему идентифицируем 4 кластера вместо 2, и все точки на границах не включены в кластеры.

from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.2, min_samples=5)
labels = dbscan.fit_predict(data)
# Count the number of clusters (excluding outliers)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
print("Number of clusters from DBSCAN:", n_clusters)

# Plot the results
plt.figure()
plt.scatter(data[:, 0], data[:, 1], c=labels)
plt.title('DBSCAN Clustering')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

Если мы правильно найдем количество кластеров, то большой разницы между K-средними и DBSCAN из приведенного выше набора данных не будет. Но есть определенные наборы данных, где K-средние просто не могут найти нужные кластеры, в то время как DBSCAN работает хорошо. См., например, набор данных, представленный ниже.

ellipse1 = generate_ellipse(1000,2,1,np.pi/4,(3,2),0.1)
ellipse2 = generate_ellipse(1000,4,2,np.pi/4,(3,2),0.1)
data = np.concatenate((ellipse1, ellipse2))
plt.figure()
plt.scatter(data[:, 0], data[:, 1])
plt.title('Data')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

Теперь мы видим резкую разницу в производительности K-средних по сравнению с DBSCAN.

Но даже DBSCAN потерпит неудачу, если мы увеличим дисперсию шума, как мы видим на графиках ниже.

ellipse1 = generate_ellipse(1000,2,1,np.pi/4,(3,2),0.25)
ellipse2 = generate_ellipse(1000,4,2,np.pi/4,(3,2),0.25)
data = np.concatenate((ellipse1, ellipse2))
plt.figure()
plt.scatter(data[:, 0], data[:, 1])
plt.title('Data')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

Давайте теперь сосредоточимся на одномерном случае, когда точки данных просто лежат вдоль линии. Мы сделаем данные целыми, потому что нас интересуют точки данных, лежащие в сетке, а целые числа всегда можно масштабировать до разрешения сетки. Одно ключевое преимущество, которое мы имеем для одномерного случая, заключается в том, что данные можно легко упорядочить, просто отсортировав.

data = np.sort(np.random.choice(np.arange(1, 100), size=10, replace=False))
print(data)

Неортодоксальный способ кластеризации здесь состоит в том, чтобы смотреть на точки данных как на места, где есть ненулевое значение. Затем мы можем сгруппировать точки данных вместе с помощью «сглаживания». См., например, следующий код. Мы создаем импульсный вектор, присваивая 1 всем местоположениям точек данных. Сглаживание выполняется путем свертки этого вектора импульса с гауссовским импульсом. Затем центроиды оцениваются путем определения пиков в результирующем сигнале.

t = np.linspace(-1, 1, 11) 
i, q, e = signal.gausspulse(t, fc=1.5, retquad=True, retenv=True)

impvec = np.zeros(100)
impvec[data] = 1

clustered_signal = np.convolve(impvec,e)
clustered_signal = clustered_signal[5:104]
peak_locs, _ = signal.find_peaks(clustered_signal);
print("Estimated centroid locations: ", peak_locs)

fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(15, 3))

ax1.plot(e, '--*', color='red')
ax1.set_title('Gaussian Pulse')
ax2.stem(impvec)
ax2.set_title('Impulse vector')
ax3.plot(clustered_signal,'-*')
ax3.stem(peak_locs,clustered_signal[peaks_locs],linefmt='r-', markerfmt='ro', basefmt='r-')
ax3.stem(data,impvec[data],linefmt='g-', markerfmt='g+', basefmt='g-')
ax3.set_title('Clustered Signal and Centroids')
plt.show()

Давайте теперь применим DBSCAN к тому же набору данных и посмотрим, что мы получим для центроидов.

# Perform DBSCAN on the data
dbscan = DBSCAN(eps=3, min_samples=2)
dbscan.fit_predict(data.reshape(-1, 1))

# Plot the results
labels = dbscan.labels_
unique_labels = set(labels)
#print(unique_labels)

# Count the number of clusters (excluding outliers)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
print("Number of DBSCAN clusters: ", n_clusters)

centroids_dbscan = []
for k in unique_labels:
    if k == -1:
        # For noise points, use the original point as the centroid
        #centroids_dbscan.append(data[labels == k])
        centroids_dbscan = np.concatenate((centroids_dbscan, data[labels == k]))
    else:
        class_members = (labels == k)
        this_centroid = np.mean(data[class_members])
        centroids_dbscan.append(this_centroid)
print("DBSCAN Centroids:", np.sort(centroids_dbscan))

Вспомните, что алгоритм максимизации ожидания (EM) начинается со случайных центроидов заранее определенного числа и находит все точки данных, которые близки к каждому из этих центроидов, перед повторным вычислением центроидов на основе ассоциаций. Теперь мы объединяем алгоритм EM с эпсилон-шаром DBSCAN, чтобы получить алгоритм, который начинается с первой точки данных в качестве центроида, и для каждой дополнительной точки данных, которая находится внутри эпсилон-шара центроида, новый центроид вычисляется путем усреднения центроид с новой точкой данных. Если следующая точка данных находится за пределами эпсилон-шара центроида, она инициирует новый кластер и становится первым центроидом для этого кластера. Нам просто нужно выполнить один проход по данным, чтобы найти количество кластеров и их центроиды. Обратите внимание, что это работает для одномерного случая, поскольку данные могут быть упорядочены!

epsilon = 5
centroids_em = np.zeros(data.shape);
centroids_em[0] = data[0];
cluster_idx = 0;
for k in range(1, len(data)):
    if abs(data[k] - centroids_em[cluster_idx]) < epsilon:
        centroids_em[cluster_idx] = 0.5 * (centroids_em[cluster_idx] + data[k])
    else:
        cluster_idx += 1
        centroids_em[cluster_idx] = data[k]

centroids_em = centroids_em[0:cluster_idx+1]
print("EM Centroids:", centroids_em)

И теперь мы, наконец, сравним различные способы кластеризации одномерных данных. На следующем графике мы видим, что результаты различных алгоритмов кластеризации почти совпадают.

plt.figure()
plt.stem(data,4*impvec[data],linefmt='g-', markerfmt='g+', basefmt='g-',label='data')
plt.stem(peaks_locs,clustered_signal[peaks_locs],linefmt='r-', markerfmt='ro', basefmt='r-', label='smoothing')
plt.stem(centroids_dbscan,2*np.ones(len(centroids_dbscan)),linefmt='b-', markerfmt='bs', basefmt='b-', label='dbscan')
plt.stem(centroids_em,3*np.ones(len(centroids_em)),linefmt='k-', markerfmt='kx', basefmt='k-', label='EM')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.show()