【Python機器學習實戰】聚類演算法(1)——K-Means聚類

Uniqe發表於2021-12-06

實戰部分主要針對某一具體演算法對其原理進行較為詳細的介紹,然後進行簡單地實現(可能對演算法效能考慮欠缺),這一部分主要介紹一些常見的一些聚類演算法。


K-means聚類演算法

0.聚類演算法演算法簡介

  聚類演算法算是機器學習中最為常見的一類演算法,在無監督學習中,可以說聚類演算法有著舉足輕重的地位。

  提到無監督學習,不同於前面介紹的有監督學習,無監督學習的資料沒有對應的資料標籤,我們只能從輸入X中去進行一些知識發現或者預處理。

  過去在有監督學習中,我們(讓機器)通過X去預測Y,而到了無監督學習中,我們(讓機器)只能從X去發現什麼,或者X中哪些輸入對我們是有用的,因此:

  無監督學習中包括了兩大方面:聚類和降維

  在無監督學習中,我們通過X可以發現什麼。聚類就是主要回答這一類問題。而對於一個具有很多維的資料,那些維度對於我們想要知道的事情的影響比較大,這就是降維要做的事情。

  聚類演算法,顧名思義,就是一種能將屬於同類別的資料聚集在一起的演算法,稱之為“物以類聚”。聚類的目的就是將相似的物件歸為同一簇中,不相似的物件歸到不同簇中。

  聚類演算法比較常用的有K-means、層次聚類演算法、DBSCAN等,這些後面會一一介紹,本節主要是K-means演算法。

  具體聚類演算法有很多應用場景,如客戶群體分析、社交網路分析等,還有很多間接的應用,如資料預處理、半監督學習等。

1.K-means聚類原理

  K-means聚類就是給定一組資料,以及一個k值,然後把這些資料分為k個類別的演算法。其中k是事先需要給定的引數。每一個簇(類別)通過這個簇的中心(質心)進行描述。大概就是下面這樣子:

  K-means演算法是聚類演算法中較為簡單的一種,原理簡單,易於實現。其原理大致是:首先給定k箇中心(質心),然後將資料分別劃分到k個簇中去,也就是說把每個資料分到距離其最近的那個中心所在的簇中。

  然後重新計算每個簇的中心,即屬於這個簇的樣本的均值即為新的簇的中心。

  具體演算法流程如下:

    1.初始化k個點作為聚類中心(通常隨機選擇)

    2.計算每個樣本距離k個聚類中心的距離,然後將每個樣本分到距離其最近的中心所屬的簇中;

    3.重新計算k個簇的中心,中心為簇中所包含資料點的均值,;

    4.重複2~3,知道k個聚類中心不再移動。

  圖片來源於網路,這裡稍微整理了一下。

    上面就是K-means演算法的過程,下面我們先對上面的過程做一個簡單的實現:

from numpy import *


# 定義距離計算
def cal_dist(vect_a, vect_b):
    return sqrt(sum(power(vect_a, vect_b, 2)))


# 隨機選取聚類中心
def rand_center(data, k):
    n = shape(data)[1]
    centroids = mat(zeros((k, n)))
    for j in range(n):
        min_j = min(data[:, j])
        range_j = float(max(data[:, j]) - min(data[:, j]))
        # np.random.rand(k, 1) 生成size為(k,1)的0~1的隨機array
        centroids[:, j] = min_j + range_j * random.rand(k, 1)
    return centroids


def Kmeans(data, k, dis_meas=cal_dist, create_center=rand_center):
    m = shape(data)[0]
    # 用於儲存每個樣本所屬類別的矩陣,第0維為所屬類別,第一維為樣本距離該類別的距離
    clusterAssment = mat(zeros((m, 2)))
    # 初始化聚類中心
    centroids = create_center(data, k)
    cluster_changed = True
    while cluster_changed:
        cluster_changed = False
        for i in range(m):
            min_dist = inf
            min_index = -1
            for j in range(k):
                dist_ij = dis_meas(data[i, :], centroids[j, :])
                if dist_ij < min_dist:
                    min_dist = dist_ij
                    min_index = j
            # 如果樣本的類別發生了變化,則繼續迭代
            if clusterAssment[i, 0] != min_index:
                cluster_changed = True
            # 第i個樣本距離最近的中心j存入
            clusterAssment[i, :] = min_index, min_dist ** 2
        print(centroids)
        # 重新計算聚類中心
        for cent in range(k):
            # 找出資料集中屬於第k類的樣本的所有資料,nonzero返回索引值
            points_in_cluster = data[nonzero(clusterAssment[:, 0].A == cent)[0]]
            centroids[cent, :] = mean(points_in_cluster, axis=0)
    return centroids, clusterAssment

  然後定義一個讀取資料的函式,使用testdata進行測試:

def loadData(filename):
    data_mat = []
    fr = open(filename)
    for line in fr.readlines():
        cur_line = line.strip().split('\t')
        flt_line = [float(example) for example in cur_line]
        data_mat.append(flt_line)
    return mat(data_mat)
data = loadData('.\testSet.txt')
centroids, cluster_ass = Kmeans(data, 4, dis_meas=cal_dist, create_center=rand_center)

import matplotlib.pyplot as plt


data_0 = data[nonzero(cluster_ass[:, 0].A == 0)[0]]
data_1 = data[nonzero(cluster_ass[:, 0].A == 1)[0]]
data_2 = data[nonzero(cluster_ass[:, 0].A == 2)[0]]
data_3 = data[nonzero(cluster_ass[:, 0].A == 3)[0]]
plt.scatter(data_0[:, 0].A[:, 0], data_0[:, 1].A[:, 0])
plt.plot(centroids[0, 0], centroids[0, 1], '*', markersize=20)
plt.scatter(data_1[:, 0].A[:, 0], data_1[:, 1].A[:, 0])
plt.plot(centroids[1, 0], centroids[1, 1], '*', markersize=20)
plt.scatter(data_2[:, 0].A[:, 0], data_2[:, 1].A[:, 0])
plt.plot(centroids[2, 0], centroids[2, 1], '*', markersize=20)
plt.scatter(data_3[:, 0].A[:, 0], data_3[:, 1].A[:, 0])
plt.plot(centroids[3, 0], centroids[3, 1], '*', markersize=20)

2.關於K-means演算法的問題和改進

  K-means的損失函式為資料點與資料點所在的聚類中心之間的距離的平方和,也就是:

  其中μ為資料點所在的類別的聚類中心,我們期望最小化損失,從而找到最佳的聚類中心和資料所屬的類別。

2.1 陷入區域性最小值問題及改進

  然而,上面說到,在K-means演算法的第一步是隨機選取k個位置作為聚類中心,這可能就會導致,不同的初始位置,對最終的聚類結果有著很大的影響,比如當把k設定為2時:

  第一次隨機選取的兩個聚類中心為左邊這張圖的位置,那麼結果可能分為上下兩個簇,當第二次選取的為右邊那張圖片的位置時,可能最終的聚類結果為左右兩個簇。

  因此,聚類中心的初始位置,對於我們最終的結果影響很大。再比如:

  上面通過K-means將資料聚為3類,但由於聚類中心的問題,導致效果不好,“+”為最終聚類中心位置,此時聚類中心已不再更新。

  這是因為K-means演算法收斂到了區域性最小值,而非全域性最小值。

  因此,我們需要一定的處理方法,來處理這樣的問題:

  一種最直觀的做法是,我們通過多次初始化聚類中心,執行K-means聚類,得到多個結果,然後比較最後的損失(上面損失函式計算方法),選擇其中最小的那一個結果

  然而這種對於k取值比較的少的時候可以這麼做,但是如果k值過大,這樣做也不會得到較好的改善。

  另一種做法是對生成的簇進行後處理,通過將具有最大的損失的那個簇,再分成兩個簇,也就是對於損失最大的那個簇再運用一次K-means演算法,將k值設為2

  但為了保證簇的總數不變,再將某兩個簇進行合併,比如上面圖中,將下面圓圈和方塊進行合併,但是如果是一個多維資料,無法進行視覺化時,我們無法檢視應該去合併哪幾個簇

  因此,可以有兩種方法去衡量:(1)將最近的聚類中心對應的類進行合併;(2)合併兩個使總的損失增加幅度最小的簇。

  第一種方法是計算聚類中心(這裡已經將最大的簇又分為兩個簇)之間的距離,將最近的兩個簇進行合併;

  第二種方法需要計算合併兩個簇之後的損失的大小,找出最佳的合併結果。

下面介紹一種利用上面劃分簇的技術所改善的K-means演算法。

二分K-Means演算法

  二分K-means演算法是一種能夠解決演算法收斂到區域性最小值的演算法,演算法思想是:首先將所有的點作為一個簇,然後分成2個,接下來,在其中選擇一個簇進行劃分,具體選擇哪個,

  要根據選擇劃分的簇能夠使總損失降低程度最大的那個,此時簇被分為3個,然後重複上述過程,直到達到所指定的k個簇即停止。

  具體演算法過程如下:

    1.將所有資料看做為一個簇;

    2.當簇的總數目小於k時:

      對於每一個簇:

        (1)計算此時的損失;

        (2)在此簇上採用K-means聚類(k=2)

        (3)計算劃分後的損失;

      選擇使劃分後損失最小的簇,將其進行劃分

  上述過程中也可以選擇損失最大的那個簇進行劃分

下面是具體實現過程:

def bi_kmeans(data, k, dist_measure=cal_dist):
    m = shape(data)[0]
    cluster_ass = mat(zeros((m, 2)))
    # 初始化聚類中心,此時聚類中心只有一個,因此對資料取平均
    centroid0 = mean(data, axis=0).tolist()[0]
    # 儲存每個簇的聚類中心的列表
    centList = [centroid0]
    for j in range(m):
        cluster_ass[j, 1] = dist_measure(mat(centroid0), data[j, :]) ** 2

    while (len(centList)) < k:
        lowestSSE = inf
        for i in range(len(centList)):
            # 在當前簇中的樣本點
            point_in_current_cluster = data[nonzero(cluster_ass[:, 0].A == i)[0], :]
            # 在當前簇運用kmeans演算法,分為兩個簇,返回簇的聚類中心和每個樣本點距離其所屬簇的中心的距離
            centroid_mat, split_cluster_ass = Kmeans(point_in_current_cluster, 2, dist_measure)
            # 計算被劃分的簇,劃分後的損失
            sse_split = sum(split_cluster_ass[:, 1])
            # 計算沒有被劃分的其它簇的損失
            sse_not_split = sum(cluster_ass[nonzero(cluster_ass[:, 0].A != i)[0], 1])
            # 選擇最小的損失的簇,對其進行劃分
            if sse_split + sse_not_split < lowestSSE:
                # 第i個簇被劃分
                best_cent_to_split = i
                # 第i個簇被劃分後的聚類中心
                best_new_centers = centroid_mat
                # 第i個簇的樣本,距離劃分後所屬的類別(只有0和1)以及距離聚類中心的距離
                best_cluster_ass = split_cluster_ass
                lowestSSE = sse_split + sse_not_split
        # 把新劃分出來的簇,屬於1類的簇重新進行編號,編號為原先的總類別數目,比如原先有兩類,選擇一個進行劃分後,又分成兩類,等於1的那一類編號為2
        best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 1)[0], 0] = len(centList)
        # 同理,屬於第0類的重新編號,編號為所選的那一類的編號
        best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 0)[0], 0] = best_cent_to_split
        # 將原來的聚類中心進行替換
        centList[best_cent_to_split] = best_new_centers[0, :]
        # 並加入新的1類的聚類中心
        centList.append(best_new_centers[1, :])
        # 將之前被選到劃分的那一類的結果全部替換成被劃分後的結果
        cluster_ass[nonzero(cluster_ass[:, 0].A == best_cent_to_split[0]), :] = best_cluster_ass

    return mat(centList), cluster_ass

 

 

2.2 K值的影響及其改進

  K-means演算法的K值需要人為給出所需要聚類的類別數目k,那麼k的選擇對於結果影響很大,通常k值的選擇通常需要根據實際進行手動選擇,比如給定一組身高和體重的資料,我們可以聚成三類(S、M、L)來指導生產。

  還有一種方法用於選擇k值的方法,成為“手肘法則”(elbow method),其原理很簡單,就是通過設定不同的k值,然後畫出每一個k值對應的損失,如圖所示:

 

 

  然後找出“手肘”的位置,就是最佳的聚類數目k。

  這裡有個問題,損失不應該是越小越好嗎?理論上是這樣的,但當我們把n個資料點聚成n個類別,此時損失為0,然而這並沒有什麼意義(類似於過擬合)。

  因此我們選擇“手肘”的位置,此時損失下降相較於後面下降速度較大,即k=3。

  具體的實現這裡不再說了,就是計算不同k值,然後計算Loss損失就可以了。這裡補充一個關於能夠自動選擇k值的庫:yellowbrick,程式碼很簡單(參考https://www.zhihu.com/question/279825061/answer/1686762604):

from sklearn.cluster import KMeans
from yellowbrick.cluster.elbow import kelbow_visualizer
from yellowbrick.datasets.loaders import load_nfl

x, y = load_nfl()

kelbow_visualizer(KMeans(random_state=4), x, k=(2, 10))

  可以自動選出k值(k=4),而且畫出的圖也很好看。

  上面就是K-means演算法的基本內容,由於演算法比較簡單,內容不多,而且sklearn也帶有Kmeans的工具包(上面那個例子裡就是)。總的來說雖然K-means演算法比較簡單,但是用途還是比較廣泛的。


最近事情比較多,學習進度有點慢~,後面會繼續針對聚類演算法做總結和學習,下一次主要對層次聚類和DBSCAN進行一個回顧和總結。

 

相關文章