機器學習回顧篇(9):K-means聚類算法


 

注:本系列所有博客將持續更新並發布在githubgitee上,您可以通過github、gitee下載本系列所有文章筆記文件。

1 聚類

本文我們來總結K-means算法。

與之前介紹過的諸多分類算法不同,K-means算法屬於聚類算法的范疇。說到這里,必須得所以說分類算法與聚類算法的區別。

分類算法與聚類算法最本質的區別在於分類算法是一種有監督學習方法,這一類算法在真正用於實踐前,必須通過已知樣本標簽的數據進行訓練,以建立樣本特征屬性到樣本標簽的最佳擬合模型;與之相反,無監督學習方法可以在沒有任何先驗知識的情況下進行分類,這種方法根據相似性原則,將具有較高相似度的數據對象划分至同一類簇,將具有較高相異度的數據對象划分至不同類簇,最終實現對數據集的分類。

2 K-means算法

2.1 算法思想

K-means算法是一種基於距離的聚類算法,這類聚類算法以距離來度量對象間的相似性,兩樣本對象間距離越大,相似性越小。關於K-means算法,有一個非常經典的故事:

有4個牧師去郊區村庄授課,剛開始,4個牧師在村庄里分別隨機選了一個位置,然后將位置公布給全村村民,村民收到消息后,紛紛選擇最近的一個牧師那里去聽課。牧師授課時,眾多村民反饋路途太遠,於是牧師記錄了來聽自己授課的所有村民的居住地址,第二次授課時,牧師選擇自己記錄的村民的中心位置作為新的授課位置,然后將位置公布給全村村民,村民收到4位牧師新的授課位置后,同樣根據距離選擇最近的牧師去聽課。之后4位牧師的每次一次授課都根據來聽自己講課的村民登記的居住地址來更新下一次授課的位置,而村民也更新4位牧師更新的位置來選擇授課牧師,直到村民的選擇不再發生變化,則牧師授課的位置也徹底穩定下來。

K-means算法思想與上面故事中牧師選位所表現出來的原理是十分相似的,最終的目的都是實現所有樣本數據(村民)到聚類中心(牧師)的距離之和最小化。K-means算法實現步驟如下:

輸入:數據集$D = \{ {x_1},{x_2}, \cdots ,{x_n}\} $,聚類個數$k$

輸出:聚類結果類簇

(1)隨機初始化$k$個樣本作為聚類中心$\{ {\mu _1},{\mu _2}, \cdots ,{\mu_k}\} $;

(2)計算數據集中所有樣本$x_i$到各個聚類中心$\mu_j$的距離$dist({x_i},{\mu _j})$,並將$x_i$划分到距離最小的聚類中心所在類簇中;

(3)對於每一個類簇,更新其聚類中心:${\mu _i} = \frac{1}{{|{c_i}|}}\sum\limits_{x \in {c_i}} x $

(4)重復(2)(3)步驟,直到聚類中心不再有明顯變化或滿足迭代次數。

總結而言,K-means算法整個流程可總結為一個優化問題,通過不斷迭代使得目標函數收斂,K-means算法目標函數為: $$J = \sum\nolimits_{j = 1}^k {\sum\nolimits_{i = 1}^n {dist({x_i},{\mu _j})} } $$

從目標函數中可以看出,有兩個因素對聚類結果有着至關重要的影響:$k$值、距離度量方式。

對於$k$值,這是K-means算法一個繞不開的問題,直接影響着最終聚類結果的准確性,在如何確定$k$值問題上,傳統的的K-means算法在對數據分布未知的情況下只能通過多次嘗試不同的$k$值來探索最優取值。值得一說的是,眾多專家學者針對K-means算法中如何確定$k$值、甚至避開$k$值的的問題對K-means算法進行優化改進,設計了許多改進的K-means算法,這又是一個大話題了,本文不在深究。
下面在說說距離度量的問題。

2.2 距離度量

對於K-means算法這類基於距離(其他還有基於密度、網格、層次、模型)的聚類算法,距離度量貫穿了算法的整個流程,所以選擇一種合適的距離度量方式尤為重要。
這里列舉幾種常見的距離度量方式。

(1)閔可夫斯基距離(Minkowski Distance) 對於兩個給定的$d$維數據樣本$X = ({x_1},{x_2}, \cdots ,{x_p})$,$Y = ({y_1},{y_2}, \cdots ,{y_p})$,閔可夫斯基距離定義為: $$dist(X,Y) = \root p \of {\sum\limits_{i = 1}^d {|{x_i} - {y_i}{|^p}} } $$

當$p=1$時,閔可夫斯基距離又被稱為曼哈頓距離(Manhattan Distance): $$dist(X,Y) = \sum\limits_{i = 1}^d {|{x_i} - {y_i}|} $$ 曼哈頓距離可以看做是數據樣本在各維度差值的絕對值之和,又被稱為折線距離、街區距離。相比於歐氏距離,曼哈頓距離沒進行平方運算,所以對離群點不敏感。

當$p=2$時,閔可夫斯基距離就是我們熟悉的歐氏距離: $$dist(X,Y) = \sqrt {\sum\limits_{i = 1}^d {|{x_i} - {y_i}{|^2}} } $$

(2)余弦距離(Cosine)

余弦距離以兩向量夾角余弦值來反映相似度,取值在$[0,1]$之間,值越大,相似度越大。
$$dist(X,Y) = \cos (X,Y) = \frac{{\sum\nolimits_{i = 1}^d {{x_i}{y_i}} }}{{\sqrt {\sum\nolimits_{i = 1}^d {{{({x_i})}^2}} } \sqrt {\sum\nolimits_{i = 1}^d {{{({y_i})}^2}} } }}$$

余弦距離在文本識別中應用比較普遍。

(3)切比雪夫距離 (Chebyshev Distance )

切比雪夫距離是以各維度差值的最大值作為最終的相似度: $$dist(X,Y) = \mathop {\max }\limits_i (|{x_i} - {y_i}|)$$

除了這三個常用距離外,還有馬氏距離、皮爾遜相關系數、KL散度等等距離度量方法。

2.3 算法總結

K-means算法是一個應用十分廣泛、出場率極高的一個聚類算法,思想簡單,解釋性強,設定好$k$值后即可輸出指定數量的類簇。不過,在實際應用中,也需要注意K-means算法的不足之處:

  • K-means算法的$k$值必須在聚類前確定,在缺乏對數據集分布認知的情況下這往往是很難估計的,只能通過多次的嘗試探索最佳的$k$值。
  • K-means算法第一次迭代時的$k$個聚類中心對算法最終結果有很大影響,但在K-means算法中,第一次迭代的$k$各聚類中心是隨機選定的,這給算法聚類結果帶來了不確定性。
  • K-means算法對非球狀分布的數據集上表現不佳。K-means算法這類基於距離的聚類算法基本假設是同一類簇內部對象間距離遠小於不同類簇間對象距離,這種假設相當於將類簇看作是一個球狀,所以對非球狀分布的數據集,K-means算法表現可能並不佳。
  • K-means算法在不斷迭代過程中使得算法逐漸優化,在每一次迭代中,都必須計算每一個對象與聚類中心的距離,所以當數據量非常大時,時間開銷比較大。

天下沒有免費的誤差,也沒有適合所有場景的算法,想要享受算法有點,就必須承受算法的不足,根據實際應用選擇合適的算法才是最佳選擇。

3 python實現K-means算法

In [1]:
import numpy as np
import matplotlib.pyplot as plt 
import copy
 

先創建一個包含3個類簇的數據集:

In [2]:
a = np.random.normal(20,5,300)
b = np.random.normal(15,5,300)
cluster1 = np.array([[x, y] for x, y in zip(a,b)])
In [3]:
a = np.random.normal(20,5,300)
b = np.random.normal(45,5,300)
cluster2 = np.array([[x, y] for x, y in zip(a,b)])
In [4]:
a = np.random.normal(55,5,300)
b = np.random.normal(30,5,300)
cluster3 = np.array([[x, y] for x, y in zip(a,b)])
In [5]:
dataset = np.append(np.append(cluster1,cluster2, axis=0),cluster3, axis=0)
 

展示一下剛創建的數據集:

In [6]:
for i in dataset:
    plt.scatter(i[0], i[1],c='black',s=6)
plt.show()
 
 

定義一個方法用於計算歐氏距離:

In [7]:
def calc_dist(simple1, simple2):
    """計算兩數據對象間的歐氏距離"""
    return np.linalg.norm(simple1-simple2) 
 

定義一個方法用於算法第一次迭代時隨機出世后聚類中心:

In [8]:
def init_centers(k, dataset):
    """隨機獲取k個初始化聚類中心"""
    shuffle_array = np.arange(dataset.shape[0])
    np.random.shuffle(shuffle_array)
    center_index = shuffle_array[:k]  # 獲取k個隨機索引
    center_dict = {}
    for i in range(k):
        center = dataset[center_index[i]]  # 聚類中心
        center_dict[i] = center
    return center_dict
In [9]:
def k_means(k,dataset):
    """實現K-means算法"""
    ds = copy.deepcopy(dataset)  # 復制一份數據
    
    epoch = 0 # 迭代次數
    center_dict = init_centers(k, ds)  # 第一次迭代時,隨機初始化k個聚類中心
    ds = np.insert(ds, 2, values=-1, axis=1)  # 插入一列作為類標簽,默認為0
    total_last = np.inf  # 上一次迭代距離總和
    while epoch<=20:  # 迭代次數少於20次時繼續迭代,也可以直接設為True,當目標函數收斂時自動結束迭代
        cluster_dist = {i:0 for i in range(k)}  # 記錄每一個類簇距離總和
        
        for simple in ds:
            min_dist = np.inf   # simple 到最近的聚類中心的距離
            min_label = -1    # 最近的聚類中心類標簽
            for label in center_dict.keys():
                dist = calc_dist(simple[:2], center_dict[label])
                if dist < min_dist:
                    min_dist = dist
                    min_label = label
            simple[2] = min_label  # 將當前樣本點划分到最近的聚類中心所在聚類中
            cluster_dist[int(min_label)] = cluster_dist[int(min_label)] + min_dist  # 更新類簇內部距離總和
        loss_now = sum(cluster_dist.values())  # 所有類簇內部距離總和
        
        print("epoch:{}, tatal distance: {}".format(epoch,loss_now))
        for i in ds:
            if i[2] == 0:
                plt.scatter(i[0], i[1],c='red',s=6)
            elif i[2] == 1:
                plt.scatter(i[0], i[1],c='green',s=6)
            else:
                        plt.scatter(i[0], i[1],c='blue',s=6)
        for center in center_dict.values():
            plt.scatter(center[0], center[1],c='black')
        plt.show()
        if total_last == loss_now:  # 如果兩次迭代距離總和都不變,證明已收斂
            break
        total_last = loss_now
        for label in center_dict.keys():  # 更新聚類中心
            simple_list = ds[ds[:,2]==label]  # 挑選出類標簽為k的所有樣本
            x = np.mean(simple_list[:, 0])
            y = np.mean(simple_list[:, 1])
            center_dict[label] = [x, y]
        epoch += 1
    
    return ds, center_dict
In [12]:
ds,cluster_label = k_means(3,dataset)
 
epoch:0, tatal distance: 12934.408559284844
 
 
epoch:1, tatal distance: 7964.838789624616
 
 
epoch:2, tatal distance: 5778.574439702343
 
 
epoch:3, tatal distance: 5628.049794639323
 
 
epoch:4, tatal distance: 5628.049794639323
 
 

好了,K-means算法就算實現好了,從上多幅圖中可以看到每一次迭代聚類中心是如何變化的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM