資料分群的目標,是把原本一大群的資料,分成一個個小群體,並將每個小群體各自用一個點來代表。最簡單的分群演算法,是 k-means clustering,這個演算法的基本步驟大致如下:

  1. 選出 k 個資料點,做為初始的中心。選取時,可以隨機地選,也可以自己設計一些原則來選。
  2. 計算每個資料點到這 k 個中心的距離。通常使用 2-norm 距離。
  3. 把跟同一個中心最近的資料點視為一群,並以其平均做為新的中心。
  4. 重複步驟 2,直到變化很小或不再變化,或者你認為重複夠多次了為止。

我們使用 scikit-learn 的 cluster.KMeans 來幫我們執行 k-means 演算法,使用的資料集是 Wine Data Set,但為了視覺化方便,範例將只使用前兩維特徵。

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_wine
from sklearn.cluster import KMeans

dataset = load_wine()
data = dataset.data[:, :2]
print('Data shapes:', data.shape)

model = KMeans(n_clusters=3)
cluster_labels = model.fit_predict(data)
cluster_centers = model.cluster_centers_

plt.subplot(1, 2, 1)
plt.plot(data[:, 0], data[:, 1], '.')

plt.subplot(1, 2, 2)
for clab in set(cluster_labels):
	idx = np.where(cluster_labels == clab)[0]
	plt.plot(data[idx, 0], data[idx, 1], '.')
	plt.plot(model.cluster_centers_[clab, 0], model.cluster_centers_[clab, 1], 'r.', markersize=10)

plt.show()

如果你想看到群聚中心移動的過程,可以一步一步地跑:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_wine
from sklearn.cluster import KMeans

dataset = load_wine()
data = dataset.data[:, :2]
print('Data shapes:', data.shape)

centers = None
for step in range(5):
    model = KMeans(
        n_clusters=3,
        init='random' if centers is None else centers,
        n_init=1,
        max_iter=1,
        random_state=0
    )
    
    cluster_labels = model.fit_predict(data)
    centers = model.cluster_centers_

    plt.subplot(1, 5, step + 1)
    for clab in set(cluster_labels):
        idx = np.where(cluster_labels == clab)[0]
        plt.plot(data[idx, 0], data[idx, 1], '.')
        plt.plot(
            centers[clab, 0],
            centers[clab, 1],
            'r.',
            markersize=10
        )
    plt.title(f'Iter {step+1}')

plt.show()

K-means 演算法在特定情況下的效果可能不太理想,其中一種情況是資料中不同維度的尺度相差太大:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_wine
from sklearn.cluster import KMeans

dataset = load_wine()
data = dataset.data[:, :2]
print('Data shapes:', data.shape)

data[:, 0] *= 100

model = KMeans(n_clusters=3)
cluster_labels = model.fit_predict(data)
cluster_centers = model.cluster_centers_

plt.subplot(1, 2, 1)
plt.plot(data[:, 0], data[:, 1], '.')

plt.subplot(1, 2, 2)
for clab in set(cluster_labels):
	idx = np.where(cluster_labels == clab)[0]
	plt.plot(data[idx, 0], data[idx, 1], '.')
	plt.plot(model.cluster_centers_[clab, 0], model.cluster_centers_[clab, 1], 'r.', markersize=10)

plt.show()

修正上述問題的方法是將輸入資料正規化,有興趣的話可以先參考相關篇章,或者自行閱讀 scikit-learn 的 preprocessing.StandardScaler,或網路上的其他相關資料。

K-means 演算法會產生不理想效果的另一種情況,是每一群資料看起來不像是實心球狀,例如同心圓:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_circles

data, _ = make_circles(n_samples=500, factor=0.5, noise=0.05)
print('Data shapes:', data.shape)

model = KMeans(n_clusters=2)
cluster_labels = model.fit_predict(data)
cluster_centers = model.cluster_centers_

plt.subplot(1, 2, 1)
plt.plot(data[:, 0], data[:, 1], '.')

plt.subplot(1, 2, 2)
for clab in set(cluster_labels):
	idx = np.where(cluster_labels == clab)[0]
	plt.plot(data[idx, 0], data[idx, 1], '.')
	plt.plot(model.cluster_centers_[clab, 0], model.cluster_centers_[clab, 1], 'r.', markersize=10)

plt.show()

在前面的幾個範例中,我們都是事先設定把資料分成多少群,並且將分群結果和每一群的中心點畫出來。若需要做更細節的控制,例如如何選擇初始點,以及演算法步驟要重複幾次等,請參考官方文件。而如何選擇群體數目,是一個非常重要的問題。其中一種決定方式是,藉由每個點到各自中心的距離總和來評估,雖然這個總和會隨著群體數目變多而減少(數學上是可以證明的,但這裡空間太小了我寫不下不是本教材的關注點所以略過),但我們通常可以選擇變化突然趨緩的地方,作為群體數目即可。下面範例會畫出群體數目分別為 1 到 10 時的距離總和:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_wine
from sklearn.cluster import KMeans

dataset = load_wine()
data = dataset.data[:, :2]
print('Data shapes:', data.shape)

dist_sum_all = []
for n in range(1, 11):
	model = KMeans(n_clusters=n)
	model.fit(data)
	dist_sum_all.append(model.inertia_)
dist_sum_all = np.array(dist_sum_all)

plt.plot(np.arange(1, 11), dist_sum_all, 'b.-')
plt.xlabel('# Clusters')
plt.ylabel('Dist')

plt.show()

另外一種分群演算法,是 DBSCAN (Density-Based Spatial Clustering of Applications with Noise),它是一種以密度為核心概念的資料分群方法,如果一個點的周圍在某個半徑內有足夠多的鄰居,便視為同一群的一部分;相反地,若點落在低密度區域,則可能被視為雜訊。它的好處是不需要事先指定群數,並且能夠處理非球狀的群集,同時對離群點具有天然的辨識能力;但相對地,分群結果對距離尺度等參數較為敏感,且在不同密度混雜的資料上容易失效。

我們以「一個點在半徑 0.2 內有五個以上的鄰居」為範例,來重新將前面會被 k-means 分壞掉的資料來分群:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_circles

data, _ = make_circles(n_samples=500, factor=0.5, noise=0.05)
print('Data shapes:', data.shape)

model = DBSCAN(eps=0.2, min_samples=5)
cluster_labels = model.fit_predict(data)

plt.subplot(1, 2, 1)
plt.plot(data[:, 0], data[:, 1], '.')

plt.subplot(1, 2, 2)
for clab in set(cluster_labels):
	idx = np.where(cluster_labels == clab)[0]
	plt.plot(data[idx, 0], data[idx, 1], '.')

plt.show()

資料分群還有許多演算法,例如以階層式的方式來進行分群等等,在此處就暫不介紹。