K-means 分群(二)


接續〈K-means 分群(一)〉,來看看另一個例子,在〈主成分分析(二)〉中看過,〈sklearn.datasets 資料集〉中的手寫數字圖片,灰階像素的特徵降維為三維後,具有群聚的現象,這暗示著灰階像素的特徵具有某種距離的概念?來試著驗證看看:

import numpy as np
import cv2
import matplotlib.pyplot as plt
from matplotlib.pyplot import cm

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans

digits = load_digits()

pca = PCA(3)  # 將 64 維投影至 3 維
projected = pca.fit_transform(digits.data)

# 透過 KMeans 分群
kmeans = KMeans(n_clusters = 10)
kmeans.fit(digits.data) 
target = kmeans.predict(digits.data)

ax = plt.axes(projection='3d')
ax.set_xlabel('component 1')
ax.set_ylabel('component 2')
ax.set_zlabel('component 3')
ax.set_box_aspect((1, 1, 1))

p = ax.scatter(
    projected[:,0], projected[:,1], projected[:,2],
    c = target,           # 指定 K-means 分群結果作為標記
    edgecolor = 'none',   # 無邊框
    alpha = 0.5,          # 不透明度
    cmap = plt.cm.get_cmap('nipy_spectral', 10) # 依標記著色
)

plt.gcf().colorbar(p) # 著色圖例

plt.show()

這邊在著色時,使用的是 KMeans 分群後的結果,來看看畫出來的樣子:

K-means 分群(二)

看來分群大致上是符合〈主成分分析(二)〉中的散佈圖結果,只不過每個群各是代表哪個數字,當然是不知道的…

另一個問題是,資料該分為幾群比較適合,例如,若事先不知道圖片代表 10 個手寫數字,該怎麼指定適當的群數呢?一個評估方式是看看分 k 群時,各群平方距離的加總。例如:

import numpy as np
import matplotlib.pyplot as plt

from scipy.spatial.distance import cdist
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans

# 各群平方距離的加總
def sum_squared_dist(data, centers, target):
    # 第 i 群平方距離加總
    def sum_square_dist(i):
        p = data[np.where(target == i)]
        return np.sum(cdist(p, [centers[i]]) ** 2)
    sum_square_dist = np.frompyfunc(sum_square_dist, 1, 1)

    k = centers.shape[0]
    return np.sum(sum_square_dist(np.arange(0, k))) 

# 分 k 群,求對應的各群平方距離加總
def k_sum_squared_dist(k, data):
    def _k_sum_squared_dist(k):
        kmeans = KMeans(n_clusters = k)
        kmeans.fit(data) 
        centers = kmeans.cluster_centers_
        target = kmeans.predict(data)
        return sum_squared_dist(data, centers, target)
    _k_sum_squared_dist = np.frompyfunc(_k_sum_squared_dist, 1, 1)

    return _k_sum_squared_dist(k)


digits = load_digits()

k = np.arange(2, 25)
msds = k_sum_squared_dist(k, digits.data)

plt.plot(k, msds);

plt.show()

實際上,KMeans 有個 inertia_ 特性,可以求得各群平方距離加總,因此上例可以改為:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_digits
from sklearn.cluster import KMeans

# 分 k 群,求對應的各群平方距離加總
def k_sum_squared_dist(k, data):
    def _k_sum_squared_dist(k):
        kmeans = KMeans(n_clusters = k)
        kmeans.fit(data) 
        return kmeans.inertia_
    _k_sum_squared_dist = np.frompyfunc(_k_sum_squared_dist, 1, 1)

    return _k_sum_squared_dist(k)


digits = load_digits()

k = np.arange(2, 25)
msds = k_sum_squared_dist(k, digits.data)

plt.plot(k, msds);

plt.show()

以上兩個範例都會顯示以下的結果:

K-means 分群(二)

可以看出隨著 k 變大,各群平方距離加總變小,這是當然,畢竟分越多群的關係,透過這種圖形,是想要找出距離加總突然變和緩的 k 值,這表示增加更多的分群,令加總變小的效應是來自於持續縮減群體大小。

這個方式稱為手肘法(Elbow method),只不過這種方式,不太適合用於維度高的資料,因為每多一個維度,就會多一個機會讓距離增加,使得坡度緩和的部份變得不明顯,試著將數字資料降維看看:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans

# 分 k 群,求對應的各群平方距離加總
def k_sum_squared_dist(k, data):
    def _k_sum_squared_dist(k):
        kmeans = KMeans(n_clusters = k)
        kmeans.fit(data) 
        return kmeans.inertia_
    _k_sum_squared_dist = np.frompyfunc(_k_sum_squared_dist, 1, 1)

    return _k_sum_squared_dist(k)

digits = load_digits()
pca = PCA(2)  # 將 64 維投影至 2 維
projected = pca.fit_transform(digits.data)

k = np.arange(2, 25)
msds = k_sum_squared_dist(k, projected)

plt.plot(k, msds);

plt.show()

這會顯示以下的結果:

K-means 分群(二)

可以比較容易看出曲線中,以大致符合後段和緩部份往前畫一直線,大約會在 10 左右有快速抬昇的趨勢,這只是一種在 k 不清楚的情況下,有個嘗試的開始,實際上是不是適合的 k,還是要進一步的思考、嘗試與分析。

例如,上面的範例其實還涉及降維,這就有一個問題了,降為 2 維是好主意嗎?在〈主成分分析(二)〉中看過,在降為二維後畫出來的圖,其實群與群之間有很大的重疊性。

群與群之間的輪廓若是明顯,重疊部份理應較小,有一種稱為輪廓係數(Silhouette coefficient)的方式可以用來評估群與群間的輪廓,計算方式為 b * a / max(a, b),a 是群中各點間的平均距離,b 是某群中各點與最接近群中各點間的平均距離,輪廓係數越大,分群的品質就越好。

sklearn 的 sklearn.metrics.silhouette_score 可以用來計算輪廓係數,來看看降為 2 維時的樣子:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans

from sklearn.metrics import silhouette_score

# 分 k 群,求對應的各群平方距離加總
def k_sum_squared_dist(k, data):
    def _k_sum_squared_dist(k):
        kmeans = KMeans(n_clusters = k)
        kmeans.fit(data) 
        # 輪廓係數
        return silhouette_score(data, kmeans.labels_)
    _k_sum_squared_dist = np.frompyfunc(_k_sum_squared_dist, 1, 1)

    return _k_sum_squared_dist(k)

digits = load_digits()
pca = PCA(2)  # 將 64 維投影至 3 維
projected = pca.fit_transform(digits.data)

k = np.arange(2, 25)
msds = k_sum_squared_dist(k, projected)

plt.plot(k, msds);

plt.show()

這會畫出以下的圖形:

K-means 分群(二)

在降為 2 維的情況下,k 在 4 左右的輪廓係數最高,如果調整降維呢?有趣的是,試著調整維度,在大約 5 維以上之後,輪廓係數大致上都是在 k 為 10 左右最高,以下是 5 維時的情況:

K-means 分群(二)

試著回頭去用手肘法對照一下:

K-means 分群(二)

大致上也是可以選擇 k 為 10 左右呢!雖然有點事後諸葛(因為我知道會有 10 數字),不過輪廓係數雖然常用來評估分群,然而若遇到分群又要降維的情況下,用來尋找降維與分群的平衡點,似乎也是個不錯的方式。

在〈K-means 分群(一)〉簡介 K-means 分群原理時,是不是覺得分群時畫出一條線,成為兩個勢力範圍的感覺?這好像跟 Voronoi 空間劃分有點像?來看看這個範例吧!

import matplotlib.pyplot as plt 
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from scipy import spatial

k = 20

X, _ = make_blobs(n_samples = 500, centers = k, cluster_std = 0.8)

kmeans = KMeans(n_clusters = k)   # 分群
kmeans.fit(X)                     
centers = kmeans.cluster_centers_ # 群心
y_kmeans = kmeans.predict(X)      # 分群

# 以群心畫 Voronoi
vor = spatial.Voronoi(centers)
spatial.voronoi_plot_2d(vor)

plt.xlabel('x')
plt.ylabel('y')

plt.scatter(
    X[:,0], X[:,1], 
    c = y_kmeans,         # 指定標記
    edgecolor = 'none',   # 無邊框
    alpha = 0.5           # 不透明度
)

plt.show() 

這會顯示以下的圖案:

K-means 分群(二)

這指出了 K-means 的另一個應用,透過不同的 k 來分群,將分群後的資料透過一些方法分析(像是方才談到的手肘法、輪廓係數等),劃分出資料間的勢力範圍,從中思考資料會有哪些群聚趨勢,甚至進一步理解這些群聚趨勢代表著什麼意義?

就像〈K-means 分群(一)〉談到的,如果你從學歷差距、收入差距、年紀、居住縣市(經緯度、離首都的遠近)等資料,找出了某種群聚趨勢,那麼這各群代表了政治取向?消費習慣嗎?

群心勢力範圍內的資料屬於同一群,如果你瞭解 Voronoi,應該會意會到每個細胞的核,就是 K-means 的群心,資料會歸屬於哪一類,是看它離哪個群心近,也就是同一群資料中群心的影響最大,這表示可以使用群心來代表這一群的資訊,以上面的範例來說,原本 500 個點的資訊,被縮減為 20 個群心的資訊。

也就是說,K-means 應用之一就是壓縮資訊,例如應用之一,是用來減少色彩資訊。例如,彩色圖片具有 RGB,RGB 的資訊可以當成點資訊畫在三維空間中,相近的色彩在三維空間中,會有距離上的相近,若將之分群,以群心位置的 RGB 來作為同群的顏色,就可以減少色彩資訊,可能的應用之一是,在列印一張彩色圖片時,你只能使用 k 個顏色。

例如,以下範例可以指定要色彩的群數:

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

k = 5

img = cv2.imread('caterpillar.jpg')
X = img.copy().reshape((img.shape[0] * img.shape[1], 3))

kmeans = KMeans(n_clusters = k)   # 分群
kmeans.fit(X)                     
centers = kmeans.cluster_centers_ # 群心
y_kmeans = kmeans.predict(X)      # 分群

# 使用群心顏色填滿同一群
img2 = centers[y_kmeans].reshape(
         (img.shape[0], img.shape[1], 3)).astype('uint8')

cv2.imshow('caterpillar', img)
cv2.imshow('reduced', img2)

cv2.waitKey(0)
cv2.destroyAllWindows()

執行結果如下:

K-means 分群(二)