K-Means聚類的Python實踐

清風無岸 7年前發布 | 13K 次閱讀 K-means Python Python開發

K-Means應該是最簡單的聚類算法之一了吧,理論上很簡單,就是隨即初始化幾個中心點,不斷的把他們周圍的對象聚集起來,然后根據這群對象的重置中心點,不斷的迭代,最終找到最合適的幾個中心點,就算完成了。

然后,真正實踐的時候才會思考的更加深入一點,比如本文的實踐內容就是一個失敗的案例(算法是成功的,場景是失敗的)。

什么是聚類

簡單的說,就是對于一組不知道分類標簽的數據,可以通過聚類算法自動的把相似的數據劃分到同一個分類中。即聚類與分類的區別主要在于,聚類可以不必知道源數據的標簽信息。

K-Means(K均值)

K均值是一種比較簡單的聚類算法,下圖是來自wiki:

從圖中可以看出,K-Means首先在空間中隨機選取三個點(彩色的),這些點可以是某個數據點所在的位置,也可以是純粹的空間隨機點。然后類似拉幫結派一樣,到自己附近“抓人”。第一輪抓完之后形成了三個穩定的新“幫派”,這時候每一個幫派由于成員發生了變化,大家就重新投票選擇新的“核心”,也就是中間點。選好新的核心之后,這個核心就又開始新一輪的拉幫結派。然后不斷的循環迭代,直到整個空間穩定時停止。

K-Means算法描述

上面對K-Means的介紹已經比較詳細了,現在,如果把K-Means算法總結成算法描述,其實只需要四步驟:

  1. 任意選擇k個點,作為初始的聚類中心。
  2. 遍歷每個對象,分別對每個對象求他們與k個中心點的距離,把對象劃分到與他們最近的中心所代表的類別中去,這一步我們稱之為“劃分”。
  3. 對于每一個中心點,遍歷他們所包含的對象,計算這些對象所有維度的和的中值,獲得新的中心點。
  4. 計算當前狀態下的損失(用來計算損失的函數叫做Cost Function,即價值函數),如果當前損失比上一次迭代的損失相差大于某一值(如1),則繼續執行第2、3步,知道連續兩次的損失差為某一設定值為止(即達到最優,通常設為1)。

距離函數

計算距離有很多種方式,我這里使用的是最簡單的歐氏距離。

損失函數(Cost Function)

每一次選取好新的中心點,我們就要計算一下當前選好的中心點損失為多少,這個損失代表著偏移量,越大說明當前聚類的效果越差,計算公式稱為(Within-Cluster Sum of Squares, WCSS):

其中,$ x_{i} $ 表示某一對象,$c_{k}$表示該對象所屬類別的中心點。整個式子的含義就是對各個類別下的對象,求對象與中心點的歐式距離的平方,把所有的平方求和就是$L(C)$。

評價標準

采用聚類的數據,通常沒有已知的數據分類標簽,所以通常就不能用監督學習中的正確率、精確度、召回度來計算了(如果有分類標簽的話,也是可以計算的)。

常用于聚類效果評價的指標為:Davies Bouldin Index,它的表達式可以寫為:

其中,$ rho_{i} $和 $ rho_{j} $ 都表示i,j兩個分類中的所有對象到中心點的平均距離。而分母中的$c_{i}$和分別表示i,j兩個分類的中心點之間的距離。整個表達式的含義就是,聚類效果越好,兩個分類間的距離應該越遠,分類內部越密集。

Python實踐

#### 一、數據準備 — 如之前所寫的樸素貝葉斯分類詳解一樣,我們的第一步依然是進行數據準備,所不同的是,由于我們不再需要對模型進行訓練,所以不必拆分原始數據為訓練和測試兩部分。

數據向量化

這部分是要格外注意的,要根據不同的數據使用不同的度量單位,比如我們現在是對新聞進行聚類,最初我使用的是,每一個單詞作為一個向量,單詞的頻度就是該維度上的值,但是后來發現結果非常差,經過請教前輩,發現對新聞聚類最好不要使用詞頻,而且要拋出新聞長度對結果的影響。

舉個例子:假如一個新聞A包含Google,Baidu兩個詞各一次,而B分別包含兩個單詞歌兩次,那么實際上他們屬于同一種分類的概率應該是一樣的,也就是距離為0才對。

另外,即便是不使用詞頻,也要注意拋棄總長度對結果的影響,比如A(Google,Baidu),而B是(Google,Baidu,Netease),那么A、B的歐式長度分別是根號2和根號3,這也不合理,我們需要正規化操作,讓他們的歐氏距離總長度都相等(參看我代碼里的normalize函數)。

二、初始化隨機點

我們的新聞數據已知屬于5個類別,所以我們就初始設定5個隨機點,我使用了random.random()函數來隨機選擇,具體代碼在initCenters函數部分。

在初始化過程完成的工作有:

  1. 第一次設定初始聚類中心
  2. 第一次為這些中心點分配對象
  3. 分配對象完成之后進行重新定位中心點
  4. 對定位后的中心點計算損失函數

三、迭代進行K-Means優化

如上面介紹K-Means算法的時候提到的,這部分需要不斷的重新劃分對象,重新定位中心點和計算損失函數,然后根據損失函數與上一次的對比決定是不是要繼續迭代。

這部分代碼在start函數內。

四、Cost Function

具體損失函數如何計算的,之前是在costFunction內,但是我發現分配對象到中心點的時候,可以順便把損失計算出來,為了提升性能,我把costFunction的代碼合并到了split函數內。

下面是完整的程序代碼:

#!/usr/bin/env python
#coding:utf-8
import os
import random
import math
class Center(object):
    def __init__(self, vector):
        self.vector = vector
        self.objects = []
class Vector(object):
    """單個數據記錄的向量表示"""
    def __init__(self, label):
        # 由于當前向量比較稀疏,所以使用字典而非數組來表示
        self.words = {}
        self.label = label
    def loadFromFile(self, file_name, word_dict):
        with open(file_name,'r') as f:
            words = f.read().split()
            for wordin words:
                if wordnot in word_dict:
                    word_dict[word] = len(word_dict)
                word_id = word_dict[word]
                self.words[word_id] = 1
    def addToNearestCenter(self, centers):
        nearest = centers[0]
        d = self.distance(centers[0].vector)
        for centerin centers[1:]:
            new_d = self.distance(center.vector)
            if new_d < d:
                d = new_d
                nearest = center
        nearest.objects.append(self)
    """
        計算兩個向量之間的歐幾里得距離,注意數據維度上的值非常稀疏.
    """
    def distance(self, vector):
        square_sum = 0.0
        for wordin vector.words:
            if wordnot in self.words:
                a = vector.words[word]
                square_sum += math.pow(a, 2)
            if wordin self.words:
                a,b = vector.words[word],self.words[word]
                square_sum += math.pow((a-b), 2)
        for wordin self.words:
            if wordnot in vector.words:
                a = self.words[word]
                square_sum += math.pow(a, 2)
        result = math.sqrt(square_sum)
        return result
class KMeans(object):
    """ 準備數據,把新聞數據向量化"""
    def __init__(self, dir_name):
        self.word_dict = {}
        self.vectors = []
        self.dir_name = dir_name
        # {'file_name':{word:3,word:4}}
        self.centers = []
        # 上一次中心點的損失
        self.last_cost = 0.0
        # 從指定目錄加載文件
        for file_namein os.listdir(dir_name):
            v = Vector(file_name)
            v.loadFromFile(dir_name+'/'+file_name, self.word_dict)
            self.vectors.append(v)
    """ 分配初始中心點,計算初始損失,并開始聚類 """
    def start(self, class_num):
        # 從現有的所有文章中,隨機選出class_num個點作為初始聚類中心
        for vectorin random.sample(self.vectors, class_num):
            c = Center(vector)
            self.centers.append(c)
        # 初始劃分,并計算初始損失
        print 'init center points'
        self.split()
        self.locateCenter()
        self.last_cost = self.costFunction()
        print 'start optimization'
        # 開始進行優化,不斷的進行三步操作:劃分、重新定位中心點、最小化損失
        i = 0
        while True:
            i += 1
            print '第 ',i,' 次優化:'
            self.split()
            self.locateCenter()
            current_cost = self.costFunction()
            print '損失降低(上一次 - 當前):',self.last_cost,' - ',current_cost,' = ',(self.last_cost - current_cost)
            if self.last_cost - current_cost  <= 1:
                break
            else:
                self.last_cost = current_cost
        # 迭代優化損失函數,當損失函數與上一次損失相差非常小的時候,停止優化
        count = 0
        for centerin self.centers:
            count += 1
            print '第', count, '組:'
            for s in ['business','it','sports','yule','auto']:
                s_count = 0
                for vectorin center.objects:
                    if vector.label.find(s) > 0:
                        s_count += 1
                print s,' = ',s_count
            print '---------------------------------------'
    """
        根據每個聚類的中心點,計算每個對象與這些中心的距離,根據最小距離重新劃分每個對象所屬的分類
    """
    def split(self):
        print '劃分對象... Objects : ', len(self.vectors)
        # 先清空上一次中心點表里的對象數據,需要重新劃分
        for centerin self.centers:
            center.objects = []
        # 遍歷所有文件并分配向量到最近的中心點區域
        for vectorin self.vectors:
            vector.addToNearestCenter(self.centers)
    """ 重新獲得劃分對象后的中心點 """
    def locateCenter(self):
        # 遍歷中心點,使用每個中心點所包含的文件重新求中心點
        count = 0
        for centerin self.centers:
            count += 1
            print '計算第 ', count, ' 類的新中心點...'
            files_count = float(len(center.objects))
            # 新的中心點,格式為 {word1:0,word2:5...}
            new_center = {}
            # 遍歷所有該中心包含的文件
            for vectorin center.objects:
                # 遍歷該文件包含的單詞
                for wordin vector.words:
                    if wordnot in new_center:
                        new_center[word] = 1
                    else:
                      # 由于不使用詞頻計算,所以出現的詞都是加1,最后再求平均
                      new_center[word] += 1
            for wordin new_center:
                new_center[word] = new_center[word]/files_count
            # 中心點對象
            center.vector = Vector('center')
            center.vector.words = new_center
    """ 損失函數 """
    def costFunction(self):
        print '開始計算損失函數'
        total_cost = 0.0
        count = 0
        for centerin self.centers:
            count += 1
            print '計算第 ', count, ' 類的損失 objects : ', len(center.objects)
            for vectorin center.objects:
                # 求距離平方作為損失
                total_cost += math.pow(vector.distance(center.vector),2)
        print '本輪損失為:',total_cost
        return total_cost
if __name__ == '__main__':
    km = KMeans('allfiles')
    km.start(5)

輸出結果:

2454  filesloaded
初始化隨機中心點個數: 5
劃分對象并計算損失...
計算第  1  點的新中心點...
計算第  2  點的新中心點...
計算第  3  點的新中心點...
計算第  4  點的新中心點...
計算第  5  點的新中心點...
第  1  次迭代:
劃分對象并計算損失...
計算第  1  點的新中心點...
計算第  2  點的新中心點...
計算第  3  點的新中心點...
計算第  4  點的新中心點...
計算第  5  點的新中心點...
損失(上一次 - 當前): 4375.51247663  -  2034.9234715  =  2340.58900512
第  2  次迭代:
劃分對象并計算損失...
計算第  1  點的新中心點...
計算第  2  點的新中心點...
計算第  3  點的新中心點...
計算第  4  點的新中心點...
計算第  5  點的新中心點...
損失(上一次 - 當前): 2034.9234715  -  2033.91112679  =  1.01234470898
第 1 組:
business  =  314
it  =  68
sports  =  15
yule  =  10
auto  =  79
---------------------------------------
第 2 組:
business  =  10
it  =  16
sports  =  26
yule  =  132
auto  =  10
---------------------------------------
第 3 組:
business  =  4
it  =  15
sports  =  4
yule  =  4
auto  =  16
---------------------------------------
第 4 組:
business  =  158
it  =  226
sports  =  388
yule  =  281
auto  =  338
---------------------------------------
第 5 組:
business  =  14
it  =  183
sports  =  40
yule  =  33
auto  =  70
---------------------------------------
 

由于初始點是隨機選擇的,所以結果并不是很好,這點可以使用k-means++算法改進,以后再寫吧。

1、對于運算速度非常慢:

瓶頸在于兩個地方,主要就是計算歐氏距離的時候,需要所有對象分別與中心點求差平方,而中心點根據實際使用,發現維度高達2W或3W,2500個文章,相當于進行2500 * 3W次的(基本運算+乘法運算),目前網上有不少庫專門做這個事情,但總結起來還是沒法根本上解決此問題。

2、對于分類效果較差:

我選擇的場景更適合用樸素貝葉斯這種概率分類或者SVM這種間隔分類,因為K-Means的效果對初始的幾個點的依賴非常大,如果運氣不好選在了邊緣或者幾個分類的中間,那效果就會奇差。

解決辦法:

避免在新聞分類或其他高維數據下使用K-Means,除非能解決初始中心點的選擇問題,關于這點,我們后面的高斯混合模型可以部分解決這個問題。

而對于性能問題我目前無解,暫時想不到合適的數據結構來簡化計算,唯一能想到的就是用C函數替代歐氏距離和損失函數的計算部分。

如果您有好的解決辦法,請聯系我QQ:83534146 ,望不吝指導,多謝!

 

來自:http://python.jobbole.com/87343/

 

 本文由用戶 清風無岸 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!