第十三篇:K-Means 聚類演算法原理分析與程式碼實現

穆晨發表於2017-01-19

前言

       在前面的文章中,涉及到的機器學習演算法均為監督學習演算法。

       所謂監督學習,就是有訓練過程的學習。再確切點,就是有 "分類標籤集" 的學習。

       現在開始,將進入到非監督學習領域。從經典的聚類問題展開討論。所謂聚類,就是事先並不知道具體分類方案的分類 (允許知道分類個數)。

       本文將介紹一個最為經典的聚類演算法 - K-Means 聚類演算法以及它的兩種實現。

現實中的聚類分析問題 - 總統大選

       假設 M 國又開始全民選舉總統了,目前 Mr.OBM 的投票率為48%(投票數佔所有選民人數的百分比),而 Mr.MKN 的為47%,而剩下的一部分出於【種種原因】沒有投票。

       做為其中某個陣營的人,自然是希望能夠儘可能的爭取到這些剩餘的票 -因為這完全可能影響最終選舉結果。

       然而,你不可能爭取到這些人的所有投票,因為你滿足某個群體的人,也許就傷害到了另一群人的利益。

       一個很不錯的想法是將這些人分為 K 個群體,然後主要對其中人數最多的幾個群體做工作。

       -- 這,就需要使用到聚類的策略了。

       聚類策略是蒐集剩餘選民的使用者資訊(各種滿意/不滿意的資訊),將這些資訊輸入進聚類演算法,然後對聚類結果中人數最多的簇的選民做思想工作。

       可能你會發現某個簇的選民都是一個社群的,一個宗教信仰的,或者具有某些共性。這樣就方便各種各樣的拉票活動了。

K-Means 聚類演算法

       K,指的是它可以發現 K 個簇;Means,指的是簇中心採用簇所含的值的均值來計算。

       下面先給出虛擬碼:

1 建立 k 個點作為起始質心 (隨機選擇):
2     當任意一個點的簇分配結果發生改變的時候:
3         對資料集中的每個資料點:
4             對每個質心:
5                 計算質心與資料點之間的距離
6             將資料點分配到距其最近的簇
7         對每一個簇:
8             求出均值並將其更新為質心

       然後是一個具體實現Python程式:

  1 #!/usr/bin/env python
  2 # -*- coding:UTF-8 -*-
  3 
  4 '''
  5 Created on 20**-**-**
  6 
  7 @author: fangmeng
  8 '''
  9 
 10 from numpy import *
 11 
 12 #==================================
 13 # 輸入:
 14 #        fileName: 資料檔名(含路徑)
 15 # 輸出:
 16 #        dataMat: 資料集
 17 #==================================
 18 def loadDataSet(fileName):
 19     '載入資料檔案'
 20     
 21     dataMat = []
 22     fr = open(fileName)
 23     for line in fr.readlines():
 24         curLine = line.strip().split('\t')
 25         fltLine = map(float,curLine)
 26         dataMat.append(fltLine)
 27     return dataMat
 28 
 29 #==================================================
 30 # 輸入:
 31 #        vecA: 樣本a
 32 #        vecB: 樣本b
 33 # 輸出:
 34 #        sqrt(sum(power(vecA - vecB, 2))): 樣本距離
 35 #==================================================
 36 def distEclud(vecA, vecB):
 37     '計算樣本距離'
 38     
 39     return sqrt(sum(power(vecA - vecB, 2)))
 40 
 41 #===========================================
 42 # 輸入:
 43 #        dataSet: 資料集
 44 #        k: 簇個數
 45 # 輸出:
 46 #        centroids: 簇劃分集合(每個元素為簇質心)
 47 #===========================================
 48 def randCent(dataSet, k):
 49     '隨機初始化質心'
 50     
 51     n = shape(dataSet)[1]
 52     centroids = mat(zeros((k,n)))#create centroid mat
 53     for j in range(n):#create random cluster centers, within bounds of each dimension
 54         minJ = min(dataSet[:,j]) 
 55         rangeJ = float(max(dataSet[:,j]) - minJ)
 56         centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
 57     return centroids
 58 
 59 #===========================================
 60 # 輸入:
 61 #        dataSet: 資料集
 62 #        k: 簇個數
 63 #        distMeas: 距離生成器
 64 #        createCent: 質心生成器
 65 # 輸出:
 66 #        centroids: 簇劃分集合(每個元素為簇質心)
 67 #        clusterAssment: 聚類結果
 68 #===========================================
 69 def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
 70     'K-Means基本實現'
 71     
 72     m = shape(dataSet)[0]
 73     # 簇分配結果矩陣。一列為簇分類結果,一列為誤差。
 74     clusterAssment = mat(zeros((m,2)))
 75     # 建立原始質心集
 76     centroids = createCent(dataSet, k)
 77     # 簇更改標記
 78     clusterChanged = True
 79     
 80     while clusterChanged:
 81         clusterChanged = False
 82         
 83         # 每個樣本點加入其最近的簇。
 84         for i in range(m):
 85             minDist = inf; minIndex = -1
 86             for j in range(k):
 87                 distJI = distMeas(centroids[j,:],dataSet[i,:])
 88                 if distJI < minDist:
 89                     minDist = distJI; minIndex = j
 90             if clusterAssment[i,0] != minIndex: clusterChanged = True
 91             clusterAssment[i,:] = minIndex,minDist**2
 92             
 93         # 更新簇
 94         for cent in range(k):#recalculate centroids
 95             ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
 96             centroids[cent,:] = mean(ptsInClust, axis=0) 
 97             
 98     return centroids, clusterAssment
 99  
100 def main():
101     'k-Means聚類操作展示'
102     
103     datMat = mat(loadDataSet('/home/fangmeng/testSet.txt'))
104     myCentroids, clustAssing = kMeans(datMat, 4)
105     
106     #print myCentroids
107     print clustAssing
108  
109 if __name__ == "__main__":
110    main()

       測試結果:

  

K-Means效能優化

       主要有兩種方式:

       1. 分解最大SSE (誤差平方和)的簇。

       PS:直接在簇內執行一次 k=2 的 K-Means 聚類即可。

       2. 合併距離最小的簇 或者 合併SSE增幅最小的兩個簇。

       基於這兩種最基本優化策略,有一種更為科學的聚類演算法 - 二分K-Means演算法,下面進行詳細介紹。

二分K-Means演算法

       該演算法大致思路為:首先將所有的點作為一個簇,然後將該簇一分為二。之後選擇其中一個簇繼續劃分。

       選擇方法自然是選擇SSE增加更小的那個方式。

       如此不斷 "裂變",直到得到使用者指定數目的簇。

       虛擬碼:

1 將所有點視為一個簇:
2     當簇數目小於k時:
3         對於每一個簇:
4             計算SSE
5             在給定的簇上面進行 k=2 的K-Means聚類
6             計算將簇一分為二後的SSE
7         選擇使得誤差最小的那個簇進行劃分操作

       具體實現函式:

 1 #======================================
 2 # 輸入:
 3 #        dataSet: 資料集
 4 #        k: 簇個數
 5 #        distMeas: 距離生成器
 6 # 輸出:
 7 #        mat(centList): 簇劃分集合(每個元素為簇質心)
 8 #        clusterAssment: 聚類結果
 9 #====================================== 
10 def biKmeans(dataSet, k, distMeas=distEclud):
11     '二分K-Means聚類演算法'
12     
13     m = shape(dataSet)[0]
14     # 聚類結果資料結構
15     clusterAssment = mat(zeros((m,2)))
16     # 原始質心
17     centroid0 = mean(dataSet, axis=0).tolist()[0]
18     centList =[centroid0]
19     
20     # 統計原始SSE
21     for j in range(m):
22         clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
23     
24     # 迴圈執行直到得到k個簇
25     while (len(centList) < k):
26         # 最小SSE
27         lowestSSE = inf
28         # 找到最適合分裂的簇進行分裂
29         for i in range(len(centList)):
30             ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
31             centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
32             sseSplit = sum(splitClustAss[:,1])
33             sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
34             
35             if (sseSplit + sseNotSplit) < lowestSSE:
36                 bestCentToSplit = i
37                 bestNewCents = centroidMat
38                 bestClustAss = splitClustAss.copy()
39                 lowestSSE = sseSplit + sseNotSplit
40     
41         # 本次劃分資訊
42         bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) 
43         bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
44         
45         # 更新簇集
46         centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]
47         centList.append(bestNewCents[1,:].tolist()[0])
48         # 更新聚類結果集
49         clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss
50         
51     return mat(centList), clusterAssment

       測試結果:

  

小結

       1. KMeans的用途很廣泛,再舉個例子吧:比如你計劃要去中國100個城市旅遊,那麼如何規劃路線呢?

       ---> 可以採用聚類的方法,將這些城市聚到幾個簇裡面,然後一個 ”簇"一個 "簇" 的進行遊玩。質心就相當於機場,誤差平方和就相當於遊玩城市到質心的距離 :)

       2. KMeans演算法是很常用的聚類演算法,然而,這裡也要提一提它的缺點:初始質心及K值的指定對結果影響較大。這個話題也衍生出很多研究論文,有興趣的讀者可以進一步研究。

相關文章