導讀:k-均值算法(英文:k-means clustering),屬於比較經常使用的算法之一,文本首先介紹聚類的理論知識包括什麼是聚類、聚類的應用、聚類思想、聚類優缺點等等;而後經過k-均值聚類案例實現及其可視化有一個直觀的感覺,針對算法模型進行分析和結果優化提出了二分k-means算法。最後咱們調用機器學習庫函數,很短的代碼完成聚類算法。(本文原創,轉載必須註明出處: 一步步教你輕鬆學K-means聚類算法
html
什麼是聚類git
統計數據分析的一門技術,在許多領域受到普遍應用,包括機器學習,數據挖掘,模式識別,圖像分析以及生物信息。聚類是把類似的對象經過靜態分類的方法分紅不一樣的組別或者更多的子集(subset),這樣讓在同一個子集中的成員對象都有類似的一些屬性,常見的包括在座標系中更加短的空間距離等。github
聚類的應用算法
在商務上,聚類能幫助市場分析人員從客戶基本庫中發現不一樣的客戶羣,而且用購買模式來刻畫不一樣的客戶羣的特徵。在生物學上,聚類能用於推導植物和動物的分類,對基因進行分類,得到對種羣中固有結構的認識。聚類在地球觀測數據庫中類似地區的肯定,汽車保險單持有者的分組,及根據房子的類型、價值和地理位置對一個城市中房屋的分組上也能夠發揮做用。聚類也能用於對Web上的文檔進行分類,以發現信息。諸如此類,聚類有着普遍的實際應用。數據庫
什麼是k-means聚類算法apache
k-平均算法(英文:k-means clustering)源於信號處理中的一種向量量化方法,如今則更多地做爲一種聚類分析方法流行於數據挖掘領域。k-平均聚類的目的是:把 n個點劃分到k個聚類中,使得每一個點都屬於離他最近的均值(此即聚類中心)對應的聚類,以之做爲聚類的標準。k-平均聚類與k-近鄰之間沒有任何關係(後者是另外一流行的機器學習技術)。app
K-Means 是發現給定數據集的 K 個簇的聚類算法, 之因此稱之爲 K-均值 是由於它能夠發現 K 個不一樣的簇, 且每一個簇的中心採用簇中所含值的均值計算而成.簇個數 K 是用戶指定的, 每個簇經過其質心(centroid), 即簇中全部點的中心來描述.
聚類與分類算法的最大區別在於, 分類的目標類別已知, 而聚類的目標類別是未知的.dom
發展歷史機器學習
雖然其思想可以追溯到1957年的Hugo Steinhaus,術語「k-均值」於1967年才被James MacQueen 首次使用。標準算法則是在1957年被Stuart Lloyd做爲一種脈衝碼調製的技術所提出,但直到1982年才被貝爾實驗室公開出版。在1965年,E.W.Forgy發表了本質上相同的方法,因此這一算法有時被稱爲Lloyd-Forgy方法。更高效的版本則被Hartigan and Wong提出。函數
算法描述
已知觀測集,其中每一個觀測都是一個 d-維實向量,k-平均聚類要把這 n個觀測劃分到k個集合中(k≤n),使得組內平方和最小。換句話說,它的目標是找到使得下式知足的聚類
,
其中 是
中全部點的均值。
k-means術語
k-means應用場景
kmeans,用於數據集內種類屬性不明晰,但願可以經過數據挖掘出或自動歸類出有類似特色的對象的場景。其商業界的應用場景通常爲挖掘出具備類似特色的潛在客戶羣體以便公司可以重點研究、對症下藥。
例如,在2000年和2004年的美國總統大選中,候選人的得票數比較接近或者說很是接近。任一候選人獲得的普選票數的最大百分比爲50.7%而最小百分比爲47.9% 若是1%的選民將手中的選票投向另外的候選人,那麼選舉結果就會大相徑庭。 實際上,若是妥善加以引導與吸引,少部分選民就會轉換立場。儘管這類選舉者佔的比例較低,但當候選人的選票接近時,這些人的立場無疑會對選舉結果產生很是大的影響。如何找出這類選民,以及如何在有限的預算下采起措施來吸引他們? 答案就是聚類(Clustering)。
那麼,具體如何實施呢?首先,收集用戶的信息,能夠同時收集用戶滿意或不滿意的信息,這是由於任何對用戶重要的內容均可能影響用戶的投票結果。而後,將這些信息輸入到某個聚類算法中。接着,對聚類結果中的每個簇(最好選擇最大簇 ), 精心構造可以吸引該簇選民的消息。最後, 開展競選活動並觀察上述作法是否有效。
另外一個例子就是產品部門的市場調研了。爲了更好的瞭解本身的用戶,產品部門能夠採用聚類的方法獲得不一樣特徵的用戶羣體,而後針對不一樣的用戶羣體能夠對症下藥,爲他們提供更加精準有效的服務。
k-means算法思想
先隨機選取K個對象做爲初始的聚類中心。而後計算每一個對象與各個種子聚類中心之間的距離,把每一個對象分配給距離它最近的聚類中心。聚類中心以及分配給它們的對象就表明一個聚類。一旦所有對象都被分配了,每一個聚類的聚類中心會根據聚類中現有的對象被從新計算。這個過程將不斷重複直到知足某個終止條件。終止條件能夠是如下任何一個:
獲得相互分離的球狀聚類,在這些聚類中,均值點趨向收斂於聚類中心。 通常會但願獲得的聚類大小大體至關,這樣把每一個觀測都分配到離它最近的聚類中心(即均值點)就是比較正確的分配方案。
k-means工做流程
建立 k 個點做爲起始質心(一般是隨機選擇) 當任意一個點的簇分配結果發生改變時(不改變時算法結束) 對數據集中的每一個數據點 對每一個質心 計算質心與數據點之間的距離 將數據點分配到距其最近的簇 對每個簇, 計算簇中全部點的均值並將均值做爲質心
k-means開發流程
收集數據:使用任意方法 準備數據:須要數值型數據類計算距離, 也能夠將標稱型數據映射爲二值型數據再用於距離計算 分析數據:使用任意方法 訓練算法:不適用於無監督學習,即無監督學習不須要訓練步驟 測試算法:應用聚類算法、觀察結果.可使用量化的偏差指標如偏差平方和(後面會介紹)來評價算法的結果. 使用算法:能夠用於所但願的任何應用.一般狀況下, 簇質心能夠表明整個簇的數據來作出決策.
k-means評價標準
k-means算法由於手動選取k值和初始化隨機質心的緣故,每一次的結果不會徹底同樣,並且因爲手動選取k值,咱們須要知道咱們選取的k值是否合理,聚類效果好很差,那麼如何來評價某一次的聚類效果呢?也許將它們畫在圖上直接觀察是最好的辦法,但現實是,咱們的數據不會僅僅只有兩個特徵,通常來講都有十幾個特徵,而觀察十幾維的空間對咱們來講是一個沒法完成的任務。所以,咱們須要一個公式來幫助咱們判斷聚類的性能,這個公式就是SSE (Sum of Squared Error, 偏差平方和 ),它其實就是每個點到其簇內質心的距離的平方值的總和,這個數值對應kmeans函數中clusterAssment矩陣的第一列之和。 SSE值越小表示數據點越接近於它們的質心,聚類效果也越好。 由於對偏差取了平方,所以更加劇視那些遠離中心的點。一種確定能夠下降SSE值的方法是增長簇的個數,但這違背了聚類的目標。聚類的目標是在保持簇數目不變的狀況下提升簇的質量。
k-means優缺點
優勢:
屬於無監督學習,無須準備訓練集
原理簡單,實現起來較爲容易
結果可解釋性較好
缺點:
聚類數目k是一個輸入參數。選擇不恰當的k值可能會致使糟糕的聚類結果。這也是爲何要進行特徵檢查來決定數據集的聚類數目了。
可能收斂到局部最小值, 在大規模數據集上收斂較慢
對於異常點、離羣點敏感
使用數據類型 : 數值型數據
咱們假設這樣的一個案例需求:某公司發佈一批新型手機,根據客戶熱衷度進行投放。公司市場人員收集其中四個地區用戶對手機的滿意程度(由兩個特徵決定的)。分析哪一個區域對手機產品比較熱衷,對應的進行市場銷售工做。這裏就用到k-means聚類算法。
上文中咱們收集好四個地區用戶對產品滿意的特徵數據值,轉化爲向量預先保存到文本中(關於詞向量轉化及其詞袋模型問題,參考:決策樹算法模型研究與案例分析一文)。咱們加載文件並以數據矩陣形式返回數據集,代碼實現以下:
'''加載數據集''' def loadDataSet(fileName): dataSet = [] # 初始化一個空列表 fr = open(fileName) for line in fr.readlines(): # 切割每一行的數據 curLine = line.strip().split('\t') # 將數據追加到dataMat,映射全部的元素爲 float類型 fltLine = list(map(float,curLine)) dataSet.append(fltLine) return mat(dataSet)
咱們打印看下結果:
上文在k均值算法思想和工做流程都提到過,咱們一個重要的方法就是隨機設置質心,而後比較每條數據(能夠理解爲單一客戶的特徵數據)與質心之間的距離。這裏距離公式包括不少,本文采用的是歐式距離計算,其代碼實現以下:
'''歐氏距離計算函數''' def distEclud(vecA, vecB): return sqrt(sum(power(vecA - vecB, 2)))
接下來,咱們構建隨機質心(中心點),這裏的K值是通過數據觀察隨機設置的值,假如k=3,表明咱們將數據集分爲3個簇,也就是說分爲3個部分。咱們隨機質心在整個數據集的邊界以內,這能夠經過找到數據集每一維的最小和最大值來完成,而後生成0到1.0之間的隨機數並經過取值範圍和最小值,以便確保隨機點在數據的邊界以內
'''
隨機質心
'''
def randCent(dataMat, k):
# 獲取樣本數與特徵值 m, n = shape(dataMat) # 初始化質心,建立(k,n)個以零填充的矩陣 centroids = mat(zeros((k, n))) # 循環遍歷特徵值 for j in range(n): # 計算每一列的最小值 minJ = min(dataMat[:, j]) # 計算每一列的範圍值 rangeJ = float(max(dataMat[:, j]) - minJ) # 計算每一列的質心,並將值賦給centroids centroids[:, j] = mat(minJ + rangeJ * random.rand(k, 1)) # 返回質心 return centroids
咱們測試下k=3的隨機質心結果:
咱們基於以上算法構建k均值算法,該算法會建立k個質心,而後將每一個點分配到最近的質心,再從新計算質心。這個過程重複數次,直到數據點的簇分配結果再也不改變位置。返回類質心與點分配結果(屢次運行結果可能會不同,能夠試試,緣由爲隨機質心的影響,但總的結果是對的,由於數據足夠類似,也可能會陷入局部最小值),代碼實現以下:
''' 建立K個質心,而後將每一個點分配到最近的質心,再從新計算質心。 這個過程重複數次,直到數據點的簇分配結果再也不改變爲止 ''' def kMeans(dataMat, k, distMeas=distEclud, createCent=randCent): # 獲取樣本數和特徵數 m, n = shape(dataMat) # 初始化一個矩陣來存儲每一個點的簇分配結果 # clusterAssment包含兩個列:一列記錄簇索引值,第二列存儲偏差(偏差是指當前點到簇質心的距離,後面會使用該偏差來評價聚類的效果) clusterAssment = mat(zeros((m, 2))) # 建立質心,隨機K個質心 centroids = createCent(dataMat, k) # 初始化標誌變量,用於判斷迭代是否繼續,若是True,則繼續迭代 clusterChanged = True while clusterChanged: clusterChanged = False # 遍歷全部數據找到距離每一個點最近的質心, # 能夠經過對每一個點遍歷全部質心並計算點到每一個質心的距離來完成 for i in range(m): minDist = inf # 正無窮 minIndex = -1 for j in range(k): # 計算數據點到質心的距離 # 計算距離是使用distMeas參數給出的距離公式,默認距離函數是distEclud distJI = distMeas(centroids[j, :], dataMat[i, :]) # 若是距離比minDist(最小距離)還小,更新minDist(最小距離)和最小質心的index(索引) if distJI < minDist: minDist = distJI minIndex = j # 若是任一點的簇分配結果發生改變,則更新clusterChanged標誌 if clusterAssment[i, 0] != minIndex: # print(clusterAssment[i, 0],minIndex) clusterChanged = True # 更新簇分配結果爲最小質心的index(索引),minDist(最小距離)的平方 clusterAssment[i, :] = minIndex, minDist ** 2 # print(centroids) # 遍歷全部質心並更新它們的取值 for cent in range(k): # 經過數據過濾來得到給定簇的全部點 ptsInClust = dataMat[nonzero(clusterAssment[:, 0].A == cent)[0]] # 計算全部點的均值,axis=0表示沿矩陣的列方向進行均值計算 centroids[cent, :] = mean(ptsInClust, axis=0)# axis=0列方向 # 返回全部的類質心與點分配結果 return centroids, clusterAssment
測試查看下運行結果:
經過上文返回的數據結果,彷佛咱們還不能直觀感覺,接下來咱們採用可視化分析方法直觀感覺下,代碼實現以下:
''' 可視化展現 ''' def kmeanShow(dataMat,centers,clusterAssment): plt.scatter(np.array(dataMat)[:, 0], np.array(dataMat)[:, 1], c=np.array(clusterAssment)[:, 0].T) plt.scatter(centers[:, 0].tolist(), centers[:, 1].tolist(), c="r") plt.show()
測試查看可視化結果:
局部最小值(局部最優的結果,但不是全局最優的結果)
上文可視化結果顯示,其中兩個簇彙集在一塊兒,也就說說沒有達到咱們預期的效果。出現這個問題有不少緣由,多是k值取的不合適,多是距離函數不合適,多是最初隨機選取的質心靠的太近,也多是數據自己分佈的問題。
爲了解決這個問題,咱們能夠對生成的簇進行後處理,一種方法是將具備最大SSE值的簇劃分紅兩個簇。具體實現時能夠將最大簇包含的點過濾出來並在這些點上運行K-均值算法,令k設爲2。
爲了保持簇總數不變,能夠將某兩個簇進行合併。從上圖中很明顯就能夠看出,應該將上圖下部兩個出錯的簇質心進行合併。那麼問題來了,咱們能夠很容易對二維數據上的聚類進行可視化, 可是若是遇到40維的數據應該如何去作?
有兩種能夠量化的辦法:合併最近的質心,或者合併兩個使得SSE增幅最小的質心。 第一種思路經過計算全部質心之間的距離, 而後合併距離最近的兩個點來實現。第二種方法須要合併兩個簇而後計算總SSE值。必須在全部可能的兩個簇上重複上述處理過程,直到找到合併最佳的兩個簇爲止。
由於上述後處理過程實在是有些繁瑣,因此有更厲害的大佬提出了另外一個稱之爲二分K-均值(bisecting K-Means)的算法.
該算法首先將全部點做爲一個簇,而後將該簇一分爲二。以後選擇其中一個簇繼續進行劃分,選擇哪個簇進行劃分取決於對其劃分時候能夠最大程度下降 SSE(平方和偏差)的值。上述基於 SSE 的劃分過程不斷重複,直到獲得用戶指定的簇數目爲止。
將全部點當作一個簇 當簇數目小於 k 時 對於每個簇 計算總偏差 在給定的簇上面進行 KMeans 聚類(k=2) 計算將該簇一分爲二以後的總偏差 選擇使得偏差最小的那個簇進行劃分操做
另外一種作法是選擇 SSE 最大的簇進行劃分,直到簇數目達到用戶指定的數目位置。
根據算法思想,咱們基於k均值算法作了少量的改動,代碼實現以下:
'''在給定數據集,所指望的簇數目和距離計算方法的條件下,函數返回聚類結果''' def biKmeans(dataMat, k, distMeas=distEclud): m, n = shape(dataMat) # 建立一個矩陣來存儲數據集中每一個點的簇分配結果及平方偏差 clusterAssment = mat(zeros((m, 2))) # 計算整個數據集的質心,並使用一個列表來保留全部的質心 centroid0 = mean(dataMat, axis=0).tolist()[0] centList = [centroid0] # [-0.15772275000000002, 1.2253301166666664] # 遍歷數據集中全部點來計算每一個點到質心的偏差值 for j in range(m): clusterAssment[j, 1] = distMeas(mat(centroid0), dataMat[j, :]) ** 2 # 對簇不停的進行劃分,直到獲得想要的簇數目爲止 while (len(centList) < k): # 初始化最小SSE爲無窮大,用於比較劃分先後的SSE lowestSSE = inf # 經過考察簇列表中的值來得到當前簇的數目,遍歷全部的簇來決定最佳的簇進行劃分 for i in range(len(centList)): # 對每個簇,將該簇中的全部點堪稱一個小的數據集 ptsInCurrCluster = dataMat[nonzero(clusterAssment[:, 0].A == i)[0], :] # 將ptsInCurrCluster輸入到函數kMeans中進行處理,k=2, # kMeans會生成兩個質心(簇),同時給出每一個簇的偏差值 centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 將偏差值與剩餘數據集的偏差之和做爲本次劃分的偏差 sseSplit = sum(splitClustAss[:, 1]) sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1]) print('sseSplit, and notSplit: ', sseSplit, sseNotSplit) # 若是本次劃分的SSE值最小,則本次劃分被保存 if (sseSplit + sseNotSplit) < lowestSSE: bestCentToSplit = i bestNewCents = centroidMat bestClustAss = splitClustAss.copy() lowestSSE = sseSplit + sseNotSplit # 找出最好的簇分配結果 # 調用kmeans函數而且指定簇數爲2時,會獲得兩個編號分別爲0和1的結果簇 bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList) # 更新爲最佳質心 bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit print('the bestCentToSplit is: ', bestCentToSplit) print('the len of bestClustAss is: ', len(bestClustAss)) # 更新質心列表 # 更新原質心list中的第i個質心爲使用二分kMeans後bestNewCents的第一個質心 centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0] # 添加bestNewCents的第二個質心 centList.append(bestNewCents[1, :].tolist()[0]) # 從新分配最好簇下的數據(質心)以及SSE clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss return mat(centList), clusterAssment
通過改進後,咱們運行biKmeans函數獲得可視化結果以下:
總結:如此咱們獲得預想的結果,解決了局部最優的問題,聚類會收斂到全局最小值。而原始的 kMeans() 函數偶爾會陷入局部最小值。
# 加載數據集 dataMat = [] fr = open("./testSet2.txt") # 注意,這個是相對路徑 for line in fr.readlines(): curLine = line.strip().split('\t') fltLine = list(map(float,curLine)) # 映射全部的元素爲 float(浮點數)類型 dataMat.append(fltLine)
km = KMeans(n_clusters=3) # 初始化 km.fit(dataMat) # 擬合 km_pred = km.predict(dataMat) # 預測 centers = km.cluster_centers_ # 質心
plt.scatter(np.array(dataMat)[:, 1], np.array(dataMat)[:, 0], c=km_pred) plt.scatter(centers[:, 1], centers[:, 0], c="r") plt.show()
源碼請進【機器學習和天然語言QQ羣:436303759】文件下載:
本文版權歸做者全部,旨在技術交流使用。未經做者贊成禁止轉載,轉載後需在文章頁面明顯位置給出原文鏈接,不然相關責任自行承擔。