【機器學習實戰】第10章 K-Means(K-均值)聚類演算法
阿新 • • 發佈:2019-01-28
第 10章K-Means(K-均值)聚類演算法
K-Means 演算法
聚類是一種無監督的學習, 它將相似的物件歸到一個簇中, 將不相似物件歸到不同簇中.
相似這一概念取決於所選擇的相似度計算方法.
K-Means 是發現給定資料集的 K 個簇的聚類演算法, 之所以稱之為 K-均值
是因為它可以發現 K 個不同的簇, 且每個簇的中心採用簇中所含值的均值計算而成.
簇個數 K 是使用者指定的, 每一個簇通過其質心(centroid), 即簇中所有點的中心來描述.
聚類與分類演算法的最大區別在於, 分類的目標類別已知, 而聚類的目標類別是未知的.
優點: 容易實現 缺點:可能收斂到區域性最小值, 在大規模資料集上收斂較慢 使用資料型別 : 數值型資料
K-Means 場景
主要用來聚類, 但是類別是未知的.
例如: 對地圖上的點進行聚類.
K-Means 術語
- 簇: 所有資料點點集合,簇中的物件是相似的。
- 質心: 簇中所有點的中心(計算所有點的均值而來).
- SSE: Sum of Sqared Error(平方誤差和), SSE 值越小,表示越接近它們的質心. 由於對誤差取了平方,因此更加註重那麼遠離中心的點.
有關 簇
和 質心
術語更形象的介紹,
請參考下圖:
K-Means 工作流程
- 首先, 隨機確定 K 個初始點作為質心(不是資料中的點).
- 然後將資料集中的每個點分配到一個簇中, 具體來講, 就是為每個點找到距其最近的質心, 並將其分配該質心所對應的簇. 這一步完成之後, 每個簇的質心更新為該簇說有點的平均值.
上述過程的 虛擬碼
如下:
- 建立 k 個點作為起始質心(通常是隨機選擇)
- 當任意一個點的簇分配結果發生改變時
- 對資料集中的每個資料點
- 對每個質心
- 計算質心與資料點之間的距離
- 將資料點分配到距其最近的簇
- 對每個質心
- 對每一個簇, 計算簇中所有點的均值並將均值作為質心
- 對資料集中的每個資料點
K-Means 開發流程
收集資料:使用任意方法 準備資料:需要數值型資料類計算距離, 也可以將標稱型資料對映為二值型資料再用於距離計算 分析資料:使用任意方法 訓練演算法:此步驟不適用於 K-Means 演算法 測試演算法:應用聚類演算法、觀察結果.可以使用量化的誤差指標如誤差平方和(後面會介紹)來評價演算法的結果. 使用演算法:可以用於所希望的任何應用.通常情況下, 簇質心可以代表整個簇的資料來做出決策.
K-Means 聚類演算法函式
從檔案載入資料集
# 從文字中構建矩陣,載入文字檔案,然後處理 def loadDataSet(fileName): # 通用函式,用來解析以 tab 鍵分隔的 floats(浮點數),例如: 1.658985 4.285136 dataMat = [] fr = open(fileName) for line in fr.readlines(): curLine = line.strip().split('\t') fltLine = map(float,curLine) # 對映所有的元素為 float(浮點數)型別 dataMat.append(fltLine) return dataMat
計算兩個向量的歐氏距離
# 計算兩個向量的歐式距離(可根據場景選擇) def distEclud(vecA, vecB): return sqrt(sum(power(vecA - vecB, 2))) # la.norm(vecA-vecB)
構建一個包含 K 個隨機質心的集合
# 為給定資料集構建一個包含 k 個隨機質心的集合。隨機質心必須要在整個資料集的邊界之內,這可以通過找到資料集每一維的最小和最大值來完成。然後生成 0~1.0 之間的隨機數並通過取值範圍和最小值,以便確保隨機點在資料的邊界之內。 def randCent(dataSet, k): n = shape(dataSet)[1] # 列的數量 centroids = mat(zeros((k,n))) # 建立k個質心矩陣 for j in range(n): # 建立隨機簇質心,並且在每一維的邊界內 minJ = min(dataSet[:,j]) # 最小值 rangeJ = float(max(dataSet[:,j]) - minJ) # 範圍 = 最大值 - 最小值 centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) # 隨機生成 return centroids
K-Means 聚類演算法
# k-means 聚類演算法 # 該演算法會建立k個質心,然後將每個點分配到最近的質心,再重新計算質心。 # 這個過程重複數次,直到資料點的簇分配結果不再改變位置。 # 執行結果(多次執行結果可能會不一樣,可以試試,原因為隨機質心的影響,但總的結果是對的, 因為資料足夠相似,也可能會陷入區域性最小值) def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): m = shape(dataSet)[0] # 行數 clusterAssment = mat(zeros((m, 2))) # 建立一個與 dataSet 行數一樣,但是有兩列的矩陣,用來儲存簇分配結果 centroids = createCent(dataSet, k) # 建立質心,隨機k個質心 clusterChanged = True while clusterChanged: clusterChanged = False for i in range(m): # 迴圈每一個數據點並分配到最近的質心中去 minDist = inf; minIndex = -1 for j in range(k): distJI = distMeas(centroids[j,:],dataSet[i,:]) # 計算資料點到質心的距離 if distJI < minDist: # 如果距離比 minDist(最小距離)還小,更新 minDist(最小距離)和最小質心的 index(索引) minDist = distJI; minIndex = j if clusterAssment[i, 0] != minIndex: # 簇分配結果改變 clusterChanged = True # 簇改變 clusterAssment[i, :] = minIndex,minDist**2 # 更新簇分配結果為最小質心的 index(索引),minDist(最小距離)的平方 print centroids for cent in range(k): # 更新質心 ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A==cent)[0]] # 獲取該簇中的所有點 centroids[cent,:] = mean(ptsInClust, axis=0) # 將質心修改為簇中所有點的平均值,mean 就是求平均值的 return centroids, clusterAssment
測試函式
在 kMeans 的函式測試中,可能偶爾會陷入區域性最小值(區域性最優的結果,但不是全域性最優的結果).
K-Means 聚類演算法的缺陷
在 kMeans 的函式測試中,可能偶爾會陷入區域性最小值(區域性最優的結果,但不是全域性最優的結果).
區域性最小值的的情況如下:
所以為了克服 KMeans 演算法收斂於區域性最小值的問題,有更厲害的大佬提出了另一個稱之為二分K-均值(bisecting K-Means)的演算法.
二分 K-Means 聚類演算法
該演算法首先將所有點作為一個簇,然後將該簇一分為二。
之後選擇其中一個簇繼續進行劃分,選擇哪一個簇進行劃分取決於對其劃分時候可以最大程度降低 SSE(平方和誤差)的值。
上述基於 SSE 的劃分過程不斷重複,直到得到使用者指定的簇數目為止。
二分 K-Means 聚類演算法虛擬碼
- 將所有點看成一個簇
- 當簇數目小雨 k 時
- 對於每一個簇
- 計算總誤差
- 在給定的簇上面進行 KMeans 聚類(k=2)
- 計算將該簇一分為二之後的總誤差
- 選擇使得誤差最小的那個簇進行劃分操作
另一種做法是選擇 SSE 最大的簇進行劃分,直到簇數目達到使用者指定的數目位置。 接下來主要介紹該做法。
二分 K-Means 聚類演算法程式碼
# 二分 KMeans 聚類演算法, 基於 kMeans 基礎之上的優化,以避免陷入區域性最小值 def biKMeans(dataSet, k, distMeas=distEclud): m = shape(dataSet)[0] clusterAssment = mat(zeros((m,2))) # 儲存每個資料點的簇分配結果和平方誤差 centroid0 = mean(dataSet, axis=0).tolist()[0] # 質心初始化為所有資料點的均值 centList =[centroid0] # 初始化只有 1 個質心的 list for j in range(m): # 計算所有資料點到初始質心的距離平方誤差 clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2 while (len(centList) < k): # 當質心數量小於 k 時 lowestSSE = inf for i in range(len(centList)): # 對每一個質心 ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] # 獲取當前簇 i 下的所有資料點 centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 將當前簇 i 進行二分 kMeans 處理 sseSplit = sum(splitClustAss[:,1]) # 將二分 kMeans 結果中的平方和的距離進行求和 sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) # 將未參與二分 kMeans 分配結果中的平方和的距離進行求和 print "sseSplit, and notSplit: ",sseSplit,sseNotSplit if (sseSplit + sseNotSplit) < lowestSSE: # 總的(未拆分和已拆分)誤差和越小,越相似,效果越優化,劃分的結果更好(注意:這裡的理解很重要,不明白的地方可以和我們一起討論) bestCentToSplit = i bestNewCents = centroidMat bestClustAss = splitClustAss.copy() lowestSSE = sseSplit + sseNotSplit # 找出最好的簇分配結果 bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) # 呼叫二分 kMeans 的結果,預設簇是 0,1. 當然也可以改成其它的數字 bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit # 更新為最佳質心 print 'the bestCentToSplit is: ',bestCentToSplit print 'the len of bestClustAss is: ', len(bestClustAss) # 更新質心列表 centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0] # 更新原質心 list 中的第 i 個質心為使用二分 kMeans 後 bestNewCents 的第一個質心 centList.append(bestNewCents[1,:].tolist()[0]) # 新增 bestNewCents 的第二個質心 clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss # 重新分配最好簇下的資料(質心)以及SSE return mat(centList), clusterAssment
測試二分 KMeans 聚類演算法
上述函式可以執行多次,聚類會收斂到全域性最小值,而原始的 kMeans() 函式偶爾會陷入區域性最小值。
執行參考結果如下: