【機器學習實戰】第10章 K-Means(K-均值)聚類算法

第 10 章 K-Means(K-均值)聚類算法

K-Means_首頁.jpg

K-Means 算法

聚類是一種無監督的學習, 它將相似的對象歸到一個簇中, 將不相似對象歸到不同簇中.
相似這一概念取決于所選擇的相似度計算方法.
K-Means 是發現給定數據集的 K 個簇的聚類算法, 之所以稱之為 K-均值 是因為它可以發現 K 個不同的簇, 且每個簇的中心采用簇中所含值的均值計算而成.
簇個數 K 是用戶指定的, 每一個簇通過其質心(centroid), 即簇中所有點的中心來描述.
聚類與分類算法的最大區別在于, 分類的目標類別已知, 而聚類的目標類別是未知的.

優點: 容易實現
缺點:可能收斂到局部最小值, 在大規模數據集上收斂較慢
使用數據類型 : 數值型數據

K-Means 場景

主要用來聚類, 但是類別是未知的.
例如: 對地圖上的點進行聚類.

K-Means 術語

  • 簇: 所有數據點點集合,簇中的對象是相似的。
  • 質心: 簇中所有點的中心(計算所有點的均值而來).
  • SSE: Sum of Sqared Error(平方誤差和), SSE 值越小,表示越接近它們的質心. 由于對誤差取了平方,因此更加注重那么遠離中心的點.

有關 質心 術語更形象的介紹, 請參考下圖:

apachecn-k-means-term-1.jpg

K-Means 工作流程

  1. 首先, 隨機確定 K 個初始點作為質心(不是數據中的點).
  2. 然后將數據集中的每個點分配到一個簇中, 具體來講, 就是為每個點找到距其最近的質心, 并將其分配該質心所對應的簇. 這一步完成之后, 每個簇的質心更新為該簇說有點的平均值.

上述過程的 偽代碼 如下:

  • 創建 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

測試函數

  1. 測試一下以上的基礎函數是否可以如預期運行, 請看: https://github.com/apachecn/MachineLearning/blob/master/src/python/10.kmeans/kMeans.py
  2. 測試一下 kMeans 函數是否可以如預期運行, 請看: https://github.com/apachecn/MachineLearning/blob/master/src/python/10.kmeans/kMeans.py

參考運行結果如下:

apachecn-k-means運行結果

在 kMeans 的函數測試中,可能偶爾會陷入局部最小值(局部最優的結果,但不是全局最優的結果).

K-Means 聚類算法的缺陷

在 kMeans 的函數測試中,可能偶爾會陷入局部最小值(局部最優的結果,但不是全局最優的結果).
局部最小值的的情況如下:

apachecn-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() 函數偶爾會陷入局部最小值。
運行參考結果如下:

apachecn-二分k-Means運行結果
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容