【机器学习】K-means算法Python实现教程
本文內容
閱讀須知:
- 閱讀本文需要有一定的Python及Numpy基礎
本文將介紹:
- K-means算法實現步驟
- 使用Python實現K-means算法
- 借助Numpy的向量計算提升計算速度
- 使用Gap Statistic法自動選取合適的聚類中心數K
K-means簡介
聚類是一個將數據集中在某些方面相似的數據成員進行分類組織的過程,聚類就是一種發現這種內在結構的技術,聚類技術經常被稱為無監督學習。
k均值聚類是最著名的劃分聚類算法,由于簡潔和效率使得他成為所有聚類算法中最廣泛使用的。給定一個數據點集合和需要的聚類數目k,k由用戶指定,k均值算法根據某個距離函數反復把數據分入k個聚類中。
K-means原理
設有樣本(x1,x2,...,xn)(x_1, x_2, ..., x_n)(x1?,x2?,...,xn?),其中每個樣本有d個特征(由d維實向量組成),K-means聚類的目標是將nnn個樣本聚類至k(≤n)k(\le n)k(≤n)個集合 S={S1,S2,...,Sk}S = \{S_1, S_2, ..., S_k\}S={S1?,S2?,...,Sk?} 中,使簇中樣本的距離和達到最小。
即用公式表示為:
arg?min?S∑i=1k∑x∈Si∥x?μi∥2=arg?min?S∑i=1k∣Si∣Var?Si{\displaystyle {\underset {\mathbf {S} }{\operatorname {arg\,min} }}\sum _{i=1}^{k}\sum _{\mathbf {x} \in S_{i}}\left\|\mathbf {x} -{\boldsymbol {\mu }}_{i}\right\|^{2}={\underset {\mathbf {S} }{\operatorname {arg\,min} }}\sum _{i=1}^{k}|S_{i}|\operatorname {Var} S_{i}} Sargmin?i=1∑k?x∈Si?∑?∥x?μi?∥2=Sargmin?i=1∑k?∣Si?∣VarSi?
其中μi\mu _iμi? 是SiS_iSi?中所有點的質心,因此上式相當于最小化簇中每兩點的平方距離:
arg?min?S∑i=1k1∣Si∣∑x,y∈Si∥x?y∥2{\displaystyle {\underset {\mathbf {S} }{\operatorname {arg\,min} }}\sum _{i=1}^{k}\,{\frac {1}{|S_{i}|}}\,\sum _{\mathbf {x} ,\mathbf {y} \in S_{i}}\left\|\mathbf {x} -\mathbf {y} \right\|^{2}} Sargmin?i=1∑k?∣Si?∣1?x,y∈Si?∑?∥x?y∥2
K-means步驟
K-means算法根據此從距離入手,迭代找出(局部)最小時的聚類中心SiS_iSi?。具體步驟如下:
Python實現
基本實現
- 需要用聚類中心數量k進行初始化,此外可以給定迭代次數與最小誤差等終止條件。
- fit函數以若干n維特征的數據作為輸入。執行后,通過classifications獲取分類結果;centroids獲取聚類中心。
- Predict函數以一個n維特征的數據作為輸入,輸出歸屬的聚類中心索引。
- 每步操作的作用詳見代碼注釋
測試代碼
blobs函數產生若干個數據點云。n_features是維度,centers是中心數目,random_state是隨機種子。
from sklearn.datasets import make_blobsdef blobs(n_samples=300, n_features=2, centers=1, cluster_std=0.60, random_state=0):points, _ = make_blobs(n_samples=n_samples,n_features=n_features,centers=centers,cluster_std=cluster_std,random_state=random_state)return points輸出聚類圖象的函數,支持2D和3D,8類別以內不同顏色區分。
def kmeans_plot(kmeans_model):"""又長又臭的函數,簡單可視化2d或3d kmeans聚類結果,不是算法必須的,直接使用即可。:param kmeans_model: 訓練的kmeans模型:type kmeans_model: Kmeans | FastKmeans"""style.use('ggplot')colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']# 2Dif kmeans_model.features_count == 2:fig = plt.figure(figsize=(12, 12))ax = fig.add_subplot()for i in range(kmeans_model.k):color = colors[i%len(colors)]for feature_set in kmeans_model.classifications[i]:ax.scatter(feature_set[0], feature_set[1], marker="x", color=color, s=50, linewidths=1)for centroid in kmeans_model.centroids:ax.scatter(centroid[0], centroid[1], marker="o", color="k", s=50, linewidths=3)# 3Delif kmeans_model.features_count == 3:fig = plt.figure(figsize=(12, 12))ax = fig.add_subplot(projection='3d')for i in range(kmeans_model.k):color = colors[i%len(colors)]for feature_set in kmeans_model.classifications[i]:ax.scatter(feature_set[0], feature_set[1], feature_set[2], marker="x", color=color, s=50, linewidths=1)for centroid in kmeans_model.centroids:ax .scatter(centroid[0], centroid[1], centroid[2], marker="o", color="k", s=50, linewidths=3)plt.show()測試調用
model = Kmeans(k=3) model.fit(blobs(centers=3, random_state=1, n_features=2)) kmeans_plot(model) model.fit(blobs(centers=3, random_state=3, n_features=3)) kmeans_plot(model)測試結果
計算效率問題
問題描述
該實現雖然可以使用,但是在進行大規模數據處理時非常慢,通過性能監控找出耗時最大的過程:
問題分析
分析后發現:距離計算的函數被調用了很多次。進而說明,主要函數(如距離)的計算單位是小規模點集,因此這些函數在python層面上調用了多次。眾所周知python的執行效率比較慢,特別是耗時排第一的norm函數,對于小規模計算的開銷是極大的。
解決辦法
最好的解決辦法是計算向量化:設法一次計算一批、甚至全部數據。這樣就可以把計算交給python底層的c語言實現;而且向量(或矩陣)的計算本身也是被優化過的。如此速度上會提升許多。
計算優化實現
numpy廣播
計算優化借助了numpy庫中向量廣播的特性。
如下圖,被減數是(N, 1, D)維向量,表示N個D維數據的數據集;減數是(1, K, D)維向量,表示K個D維的聚類中心。相減時,由于向量維度不匹配,numpy會將向量廣播為矩陣(結果為虛線表示的矩陣)再相減。而該例子中,所得結果矩陣恰好為:每個點與各個聚類中心的距離信息。
如此一來,甚至不需要顯式寫一個循環就可以完成一整輪的距離計算。這樣做把計算函數交給了受優化的矩陣運算,交給了高效的底層c實現。實現代碼
- compute_l2_distance利用了numpy廣播
速度對比
每組對比實驗分別使用兩種做法對n個數據進行從1到20類的聚類。記錄兩種做法分別的耗時。
| 普通實現 | 0.86 | 2.91 | 23.72 | 59.17 | 129.69 |
| 計算優化實現 | 0.07 | 0.98 | 3.84 | 6.41 | 28.34 |
| 比率 | 12.2857 | 2.9694 | 6.1771 | 9.2309 | 4.5762 |
可見在不同的數據規模下均有一定提升。兩種做法的速度差距在之后選擇k的過程中更加明顯。
自動K值選取
常用方法
手肘法
其思想是:
隨著聚類數k的增大,樣本劃分會更加精細,每個簇的聚合程度會逐漸提高,那么誤差平方和SSE自然會逐漸變小。
當k小于真實聚類數時,由于k的增大會大幅增加每個簇的聚合程度,故SSE的下降幅度會很大;而當k到達真實聚類數時,再增加k所得到的聚合程度回報會迅速變小,所以SSE的下降幅度會驟減,然后隨著k值的繼續增大而趨于平緩,也就是說SSE和k的關系圖是一個手肘的形狀,而這個肘部對應的k值就是數據的真實聚類數。
但該做法的缺點是需要人工判斷“手肘”位置到底在哪,在類別多時難以判斷。
Gap statistic
其定義為:
Gap(K)=E(logDk)?logDkGap(K)=E(logD_k)-logD_kGap(K)=E(logDk?)?logDk?
其中,E(logDk)E(logD_k)E(logDk?)是logDklogD_klogDk?的期望,一般使用蒙特卡洛模擬產生。
簡單解釋
模擬的基本過程是:首先在樣本所在區域內按照均勻分布隨機地產生和原始樣本數一樣多的隨機樣本,并用K-means模型對這個隨機樣本聚類,計算結果的誤差和,得到一個DkD_kDk?。重復多次就可以近似計算出E(logDk)E(logD_k)E(logDk?)。
實際上,計算E(logDk)E(logD_k)E(logDk?)只是為了給評判實際誤差logDklogD_klogDk?提供一個標準。當選取合適的KKK值時,DkD_kDk?應處于偏離(遠低于)平均值的極端情況,因此通過與期望誤差做差,得到最大Gap(K)Gap(K)Gap(K)所對應的K即是最好的選擇。
詳細解釋
- 原理性較強,非必須理解
- 本節內容翻譯自原論文
設聚類簇CrC_rCr?中兩點間距離和為:
Dr=∑i,i′∈Crdii′D_r=\sum_{i,i'\in C_r} d_{ii'} Dr?=i,i′∈Cr?∑?dii′?
若dii′d_{ii'}dii′?為歐幾里得距離,則可定義“每個簇內平方和均值”的總加和WkW_kWk?為:
Wk=∑r=1k12nrDrW_k=\sum_{r=1}^{k}\frac{1}{2n_r}D_r Wk?=r=1∑k?2nr?1?Dr?
其中因子222恰好使上式成立;數據規模nnn被約分掉了。
該方法通過與一個合適的零分布比較來標準化log?(Wk)\log(W_k)log(Wk?),而最優聚類數則是使log?(Wk)\log(W_k)log(Wk?)最偏離于上述參考分布時的值kkk。因此定義:
Gapn(k)=En?{log?(Wk)}?log?(Wk)Gap_n(k)=E_n^*\{\log(W_k)\}-\log(W_k) Gapn?(k)=En??{log(Wk?)}?log(Wk?)
其中En?E_n^*En??表示樣本規模nnn的數據在參考分布中的數學期望。考慮抽樣分布后,使Gapn(k)Gap_n(k)Gapn?(k)最大化的值就是所估計聚類數kkk的最優值。
這個方法的操作是一般化的,可以用在以任意形式計算距離dii′d_{ii'}dii′?的任意的聚類方法。
Gap Statistic的思路啟發是:假設有n個、k簇、均勻分布的p維數據點,設想它們在各自的簇中均勻分布,則log?(Wk)\log(W_k)log(Wk?)的期望近似是:
log?(pn12)?(2p)log?(k)+Constant\log(\frac{pn}{12})-(\frac{2}{p})\log(k)+Constant log(12pn?)?(p2?)log(k)+Constant
如果數據確實有K個相互分離的聚類,對于k≤Kk\le Kk≤K,log?(Wk)\log(W_k)log(Wk?)應比預期下降率(2p)log?(k)(\frac{2}{p})\log(k)(p2?)log(k)下降的更快;當k>Kk \gt Kk>K,事實上是在加入不必要的聚類中心,此時由簡單代數可得log?(Wk)\log(W_k)log(Wk?)應下降的比其預期速率更慢。因此Gapn(K)Gap_n(K)Gapn?(K)會在k=Kk=Kk=K時取得最大值。
使用Gap Statistic選取最優的k
首先,該算法需要評估聚類誤差DkD_kDk?,由于最后的標準是相對的,因此DkD_kDk?的求法并無過多約束,只要能體現其誤差即可,因此還是選擇最簡便的做法:各點到聚類中心的距離為單個誤差,將其加和作為最終的誤差,由sum_distance處理這一過程。
雖然聚類中心K的最優解是不依賴算法客觀存在的,但由于不同K-means實現會得出不同的DkD_kDk?,因此為了Gap(K)Gap(K)Gap(K)具有可比性,需要借助同一種K-means實現求解誤差DkD_kDk?,因此sum_distance中統一使用FastKmeans。
gap函數需要給定k的測試范圍ks以及蒙特卡洛模擬次數nrefs,負責產生隨機樣品估計E(logDk)E(logD_k)E(logDk?)、計算logDklogD_klogDk?,然后返回范圍內各個kkk值對應的Gap(K)Gap(K)Gap(K)值。
def gap(data, refs=None, nrefs=20, ks=range(1, 11)):shape = data.shapeif refs == None:tops = data.max(axis=0)bots = data.min(axis=0)dists = scipy.matrix(np.diag(tops - bots))rands = scipy.random.random_sample(size=(shape[0], shape[1], nrefs))for i in range(nrefs):rands[:, :, i] = rands[:, :, i] * dists + botselse:rands = refsgaps = np.zeros((len(ks),))for (i, k) in enumerate(ks):disp = sum_distance(data, k)refdisps = np.zeros((rands.shape[2],))for j in range(rands.shape[2]):refdisps[j] = sum_distance(rands[:, :, j], k)gaps[i] = np.lib.scimath.log(np.mean(refdisps)) - np.lib.scimath.log(disp)return gaps調用代碼
先生成隨機點云,此處生成了一組6個聚類中心的3維點云。
之后調用gap函數。
結果輸出
此處順便做了之前兩種實現的效率對比,下面兩個輸出分別對應K-means和Fast K-means實現。
[-0.05127935 -0.01656365 0.30313623 0.78059812 1.56439394 1.709803511.67235943 1.63859173 1.62538263 -0.84390957] best k: 6 58.349995613098145[-0.05141505 -0.01660111 0.30252902 0.78299894 1.56696863 1.706896031.67327937 1.63563849 1.62526949 -0.8487873 ] best k: 6 4.088269472122192結果分析
可以看到兩種實現的gap值(兩個列表輸出)有略微不同,但都得到k=6k=6k=6的結果,而之前生成的數據正是666個中心,說明算法成功檢測出最優的kkk值。
此外,可以看到K-means版本的gap函數運行需要58秒,而Fast K-means只需要4秒,速度差距超過十倍,之前的優化還算是效果拔群的。
Gap Statistic總結
- 理論上可以自動化找出最優的K。
- 但由于實現上需要借助特定的K-means算法,而K-means具有局部最優的特點,因此該算法找出的K并不一定是最優的。
- 再加上期望使用蒙特卡洛方法,想得到穩定、靠譜的答案需要花費更多時間進行頻率測試。
- 有時會出現多個峰值(設想3類別聚類完全可以對半分成6類),一般根據經驗主義選取第一個。
- 無論如何,用于自動化估計較優的K,Gap statistic已經足夠了。
參考文獻
[1] Tibshirani R , Hastie W T . Estimating the number of clusters in a data set via the gap statistic[J]. Journal of the Royal Statistical Society B, 2001, 63(2):411-423.
非文獻參考
【機器學習】K-means(非常詳細)
https://zhuanlan.zhihu.com/p/78798251
A Python implementation of the Gap Statistic
https://gist.github.com/michiexile/5635273
Nuts and Bolts of NumPy Optimization Part 2: Speed Up K-Means Clustering by 70x
https://blog.paperspace.com/speed-up-kmeans-numpy-vectorization-broadcasting-profiling/
總結
以上是生活随笔為你收集整理的【机器学习】K-means算法Python实现教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一对多,多对多查询
- 下一篇: 【python游戏开发入门】pygame