1. 程式人生 > 其它 >機器學習 - k-means聚類

機器學習 - k-means聚類

k-means簡介

k-means是無監督學習下的一種聚類演算法,簡單說就是不需要資料標籤,僅靠特徵值就可以將資料分為指定的幾類。k-means演算法的核心就是通過計算每個資料點與k個質心(或重心)之間的距離,找出與各質心距離最近的點,並將這些點分為該質心所在的簇,從而實現聚類的效果。

k-means具體步驟


1.指定要把資料聚為幾類,確定k值;
2.從資料點中隨機選擇k個點,作為k個簇的初始質心;
3.計算資料點與各質心之間的距離,並將最近的質心所在的簇作為該資料點所屬的簇;
4.計算每個簇的資料點的平均值,並將其作為新的質心;
5.重複步驟2-4,直到所有簇的質心不再發生變化,或達到最大迭代次數。

可以看出,k-means演算法的步驟很清晰,而且易於實現。雖然這裡的k值感覺像是拍腦袋得到的,但是後面會介紹Elbow方法,通過評估得到合理的k值。我們先來感受一下k-means的效果。

程式碼實現

import random
import math
import matplotlib.pyplot as plt


class KMeans:
    def __init__(self, n_clusters):
        self.n_clusters = n_clusters
        self.centroid_list = []
        self.predict = []

    def get_rand_centroid(self, X):
        # 隨機取質心
        centroid_list = []
        while len(centroid_list) < self.n_clusters:
            d = int(random.random() * len(X))
            if X[d] not in centroid_list:
                centroid_list.append(X[d])
        return centroid_list

    @staticmethod
    def get_distance(point, C):
        # 計算兩點間距離(歐式距離)
        return math.sqrt((point[0]-C[0])**2 + (point[1]-C[1])**2)

    def get_distributed(self, X):
        # 計算每個點距離最近的質心,並將該點劃入該質心所在的簇
        dis_list = [[] for k in range(self.n_clusters)]
        for point in X:
            distance_list = []
            for C in self.centroid_list:
                distance_list.append(self.get_distance(point, C))
            min_index = distance_list.index(min(distance_list))
            dis_list[min_index].append(point)
        return dis_list

    def get_virtual_centroid(self, distributed):
        # 如果有空集,則取其他兩個質心的座標均值
        if [] in distributed:
            index = distributed.index([])
            v_centroid_list = self.centroid_list.copy()
            # 去除空集對應的質心
            v_centroid_list.pop(index)
            # 計算其餘兩個質心的座標均值
            x = []
            y = []
            for C in v_centroid_list:
                x.append(C[0])
                y.append(C[1])
            v_centroid_list.insert(index, [round(sum(x) / len(x)), round(sum(y) / len(y))])
        else:
            # 計算每個簇所有點的座標的算數平均,作為虛擬質心
            v_centroid_list = []
            for distribution in distributed:
                x = []
                y = []
                for point in distribution:
                    x.append(point[0])
                    y.append(point[1])
                v_centroid_list.append([sum(x)/len(x), sum(y)/len(y)])
        return v_centroid_list

    def fit_predict(self, X):
        self.centroid_list = self.get_rand_centroid(X)
        while True:
            # 聚類
            distributed = self.get_distributed(X)
            # 計算虛擬質心
            v_centroid_list = self.get_virtual_centroid(distributed)
            # 如果兩次質心相同,說明聚類結果已定
            if sorted(v_centroid_list) == sorted(self.centroid_list):
                break
            # 否則繼續訓練
            self.centroid_list = v_centroid_list
        # 對結果按照資料集順序進行分類
        predict = []
        for point in X:
            i = 0
            for dis in distributed:
                if point in dis:
                    predict.append(i)
                i += 1
        self.predict = predict
        return predict

    def plot_clustering(self, X):
        x = []
        y = []
        for point in X:
            x.append(point[0])
            y.append(point[1])
        plt.scatter(x, y, c=self.predict, marker='x')
        plt.show()

# 樣本集合
X = [[0.0888, 0.5885],
     [0.1399, 0.8291],
     [0.0747, 0.4974],
     [0.0983, 0.5772],
     [0.1276, 0.5703],
     [0.1671, 0.5835],
     [0.1306, 0.5276],
     [0.1061, 0.5523],
     [0.2446, 0.4007],
     [0.1670, 0.4770],
     [0.2485, 0.4313],
     [0.1227, 0.4909],
     [0.1240, 0.5668],
     [0.1461, 0.5113],
     [0.2315, 0.3788],
     [0.0494, 0.5590],
     [0.1107, 0.4799],
     [0.1121, 0.5735],
     [0.1007, 0.6318],
     [0.2567, 0.4326],
     [0.1956, 0.4280]
    ]

# 初始化kmeans分類器
km = KMeans(3)
# 預測
predict = km.fit_predict(X)
print(predict)
# 繪圖
km.plot_clustering(X)

聚類結果如下:

我們採用了幾場籃球比賽的球員技術統計資料作為樣本,x軸表示助攻資料,y軸表示得分資料,從初步的聚類中,我們可以看出,右下角的一類可看做是助攻很多,而得分較少的球員;左側這類可看做是助攻較少,但得分相對多一些的球員;而最上面獨立的那個類,可看做是得分最多,而且助攻也不少的球員,可理解成MVP球員。這樣一來,我們就賦予了這些聚類實際的意義。感覺這個分類結果也挺不錯的。
但多跑幾次,會發現有時候的分類結果會不太一樣,例如沒有將MVP球員作為單獨的一類:

可以看出,如果首次隨機出的質心捱得太近,會導致分類結果不理想(藍色向下的箭頭表示隨機的初始質心)。

k-means++

為了解決初始質心不合理而導致分類不準確的問題,我們可以優化質心初始化的步驟,使各個質心相距儘可能的遠。k-means++ 演算法就是一種為 k-means 尋找初始化質心的演算法。它由David Arthur 和 Sergei Vassilvitskii 於2007年提出(

論文地址),主要是對之前步驟1進行進一步優化,具體步驟如下:
1a.隨機選出一個點作為質心;
1b.通過計算概率,得到新的質心。其中概率的計算方法如下:

1c.重複1b步,直到湊齊k個質心。

注:原作論文中並未提及如何根據概率值選擇質心,我看網上有很多文章使用了“輪盤法”挑選質心,就是計算出各點的概率後,再計算出各點的累計概率值,然後隨機一個概率值r,將r落入累計概率區間對應的點作為質心。這裡就粗暴一點,直接選取概率最大的那個點(與當前質心最遠的點)作為質心。

def k_means_plus(self, X):
    centroid_list = []
    # 隨機選出第一個點
    random_1 = int(random.random() * len(X))
    centroid_list.append(X[random_1])
    while len(centroid_list) < self.n_clusters:
        distance_list = []
        for x in X:
            tmp_list = []
            for C in centroid_list:
                tmp_list.append(pow(self.get_distance(x, C), 2))
            # 取距離當前質心最近的距離D(X)^2
            distance_list.append(min(tmp_list))
        # 選取D(X)^2最大的點作為下個質心(概率計算公式中分母相同,可略去)
        max_index = distance_list.index(max(distance_list))
        centroid_list.append(X[max_index])
    return centroid_list

直接拿這個函式替換之前的get_rand_centroid()方法即可。聚類效果如下:

三個初始質心都有較遠的距離,從而保證了聚類的準確性。

WCSS - k-means演算法的評估標準

由於k-means是一種無監督學習方法,沒有一種嚴格的標準來衡量聚類結果的效能,大部分情況下都是根據人的經驗來判斷,但是如果資料超過三維,無法視覺化的話,就比較尷尬了。所以還是需要有一種方法能夠評估k-means的效能。之前不是通過計算距離選取質心嗎,我們便可以用所有點到其所屬質心的距離加和作為一種衡量方式,這就是WCSS方法(Within-Cluster Sum of Squares),WCSS方法可以將效能進行量化,對於相同的k,WCSS越小,代表總體效能越好。所以我們可以進行N輪訓練,取WCSS最小的那個作為最終的聚類。
直接在fit_predict()方法中加入WCSS的計算即可:

def fit_predict(self, X, N=10):
    # 進行N輪訓練(預設進行10輪)
    train_num = 0
    # 存放質心與聚類結果
    WCSS_dict = {}
    while train_num < N:
        # self.centroid_list = self.get_rand_centroid(X)
        # 使用k-means++演算法選出初始質心
        self.centroid_list = self.k_means_plus(X)
        # 記錄初始質心
        self.initial_list = self.centroid_list
        while True:
            # 聚類
            distributed = self.get_distributed(X)
            # 計算虛擬質心
            v_centroid_list = self.get_virtual_centroid(distributed)
            # 如果兩次質心相同,說明聚類結果已定
            if sorted(v_centroid_list) == sorted(self.centroid_list):
                break
            # 否則繼續訓練
            self.centroid_list = v_centroid_list
        # 對結果按照資料集順序進行分類
        predict = []
        WCSS = 0
        for point in X:
            i = 0
            for dis in distributed:
                if point in dis:
                    predict.append(i)
                    # 計算當前點到其質心的距離,平方後再累加到WCSS中
                    WCSS += pow(self.get_distance(point, self.centroid_list[i]), 2)
                i += 1
        WCSS_dict[WCSS] = [self.centroid_list, predict]
        print("第" + str(train_num+1) + "輪的WCSS為:" + str(WCSS))
        train_num += 1
    # 選出WCSS最小的那個作為最終的聚類
    min_WCSS = min(WCSS_dict.keys())
    last_predict = WCSS_dict[min_WCSS][1]
    self.predict = last_predict
    return last_predict, min_WCSS

# 執行結果:
第1輪的WCSS為:0.04830321200000001
第2輪的WCSS為:0.04830321200000001
第3輪的WCSS為:0.04830321200000001
第4輪的WCSS為:0.04830321200000001
第5輪的WCSS為:0.04751589380952381
第6輪的WCSS為:0.04830321200000001
第7輪的WCSS為:0.04751589380952381
第8輪的WCSS為:0.04830321200000001
第9輪的WCSS為:0.04751589380952381
第10輪的WCSS為:0.04830321200000001


圖中用紅框標出的點,之前被歸為得分較出色的球員,現在被歸為了助攻較出色的球員。(紅色的*點標註的是最終的質心)

Elbow方法 - 確定k的好辦法

最後,我們再來講一下關於k的取值問題。隨著k的增大,即質心的增多,整體的WCSS是逐漸減小的(因為每個點能找到與其距離更近的質心的概率變大了),所以我們可以通過不斷增大k,來觀察整體的效能:

def elbow(self, k_range=10):
    # 通過elbow方法選擇最佳k值
    x = []
    y = []
    for k in range(1, k_range+1):
        self.n_clusters = k
        x.append(k)
        # 取出不同k值下的WCSS
        _, min_WCSS = self.fit_predict(X)
        y.append(min_WCSS)
    plt.plot(x, y)
    plt.xlabel('k')
    plt.ylabel('WCSS')
    plt.show()


從圖中可以發現,k從1到4的過程中,聚類效能有著明顯的提升,但是再繼續增加聚類的個數,效能提升的幅度就沒有之前那麼明顯了,而且,聚類個數太多,也會讓資料集變得過於分散,所以,在本例中,k=4是較好的選擇。說明一開始根據經驗取的k=3並不是最優的。我們再跑一下k=4的聚類:

通過最終的聚類,我們發現,演算法將之前得分較多的球員又細分為了兩類,黃色這類的得分要比綠色這類更高,所以可看做是真正得分多的球員;而綠色這類的得分並不出眾,助攻也不算高,可看做是表現較為一般的球員(畢竟不可能所有的球員都有好的表現)。這樣的分類從整體來看也更加符合實際情況。