資料分群的目標,是把原本一大群的資料,分成一個個小群體,並將每個小群體各自用一個點來代表。最簡單的分群演算法,是 k-means clustering,這個演算法的基本步驟大致如下:
- 選出 k 個資料點,做為初始的中心。選取時,可以隨機地選,也可以自己設計一些原則來選。
- 計算每個資料點到這 k 個中心的距離。通常使用 2-norm 距離。
- 把跟同一個中心最近的資料點視為一群,並以其平均做為新的中心。
- 重複步驟 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()資料分群還有許多演算法,例如以階層式的方式來進行分群等等,在此處就暫不介紹。