聚類算法 之 k-means

本文轉自:http://blog.pluskid.org/?p=17 

Clustering 中文翻譯作“聚類”,簡單地說就是把相似的東西分到一組,同 Classification (分類)不同,對於一個 classifier ,通常需要你告訴它“這個東西被分爲某某類”這樣一些例子,理想情況下,一個 classifier 會從它得到的訓練集中進行“學習”,從而具備對未知數據進行分類的能力,這種提供訓練數據的過程通常叫做 supervised learning (監督學習),而在聚類的時候,我們並不關心某一類是什麼,我們需要實現的目標只是把相似的東西聚到一起,因此,一個聚類算法通常只需要知道如何計算相似 度就可以開始工作了,因此 clustering 通常並不需要使用訓練數據進行學習,這在 Machine Learning 中被稱作 unsupervised learning (無監督學習)。

舉一個簡單的例子:現在有一羣小學生,你要把他們分成幾組,讓組內的成員之間儘量相似一些,而組之間則差別大一些。最後分出怎樣的結果,就取決於你對於“相似”的定義了,比如,你決定男生和男生是相似的,女生和女生也是相似的,而男生和女生之間則差別很大”,這樣,你實際上是用一個可能取兩個值“男”和“女”的離散變量來代表了原來的一個小學生,我們通常把這樣的變量叫做“特徵”。實際上,在這種情況下,所有的小學生都被映射到了兩個點的其中一個上,已經很自然地形成了兩個組,不需要專門再做聚類了。另一種可能是使用“身高”這個特徵。我在讀小學候,每週五在操場開會訓話的時候會按照大家住的地方的地域和距離遠近來列隊,這樣結束之後就可以結隊回家了。除了讓事物映射到一個單獨的特徵之外,一種常見的做法是同時提取 N 種特徵,將它們放在一起組成一個 N 維向量,從而得到一個從原始數據集合到 N 維向量空間的映射——你總是需要顯式地或者隱式地完成這樣一個過程,因爲許多機器學習的算法都需要工作在一個向量空間中。

那麼讓我們再回到 clustering 的問題上,暫且拋開原始數據是什麼形式,假設我們已經將其映射到了一個歐幾里德空間上,爲了方便展示,就使用二維空間吧,如下圖所示:

cluster

從數據點的大致形狀可以看出它們大致聚爲三個 cluster ,其中兩個緊湊一些,剩下那個鬆散一些。我們的目的是爲這些數據分組,以便能區分出屬於不同的簇的數據,如果按照分組給它們標上不同的顏色,就是這個樣子:

cluster

那麼計算機要如何來完成這個任務呢?當然,計算機還沒有高級到能夠“通過形狀大致看出來”,不過,對於這樣的 N 維歐氏空間中的點進行聚類,有一個非常簡單的經典算法,也就是本文標題中提到的 k-means 。在介紹 k-means 的具體步驟之前,讓我們先來看看它對於需要進行聚類的數據的一個基本假設吧:對於每一個 cluster ,我們可以選出一箇中心點 (center) ,使得該 cluster 中的所有的點到該中心點的距離小於到其他 cluster 的中心的距離。雖然實際情況中得到的數據並不能保證總是滿足這樣的約束,但這通常已經是我們所能達到的最好的結果,而那些誤差通常是固有存在的或者問題本身的不可分性造成的。例如下圖所示的兩個高斯分佈,從兩個分佈中隨機地抽取一些數據點出來,混雜到一起,現在要讓你將這些混雜在一起的數據點按照它們被生成的那個分佈分開來:

gaussian

由於這兩個分佈本身有很大一部分重疊在一起了,例如,對於數據點 2.5 來說,它由兩個分佈產生的概率都是相等的,你所做的只能是一個猜測;稍微好一點的情況是 2 ,通常我們會將它歸類爲左邊的那個分佈,因爲概率大一些,然而此時它由右邊的分佈生成的概率仍然是比較大的,我們仍然有不小的機率會猜錯。而整個陰影部分是我們所能達到的最小的猜錯的概率,這來自於問題本身的不可分性,無法避免。因此,我們將 k-means 所依賴的這個假設看作是合理的。

基於這樣一個假設,我們再來導出 k-means 所要優化的目標函數:設我們一共有 N 個數據點需要分爲 K 個 cluster ,k-means 要做的就是最小化

這個函數,其中 r_{nk} 在數據點 n 被歸類到 cluster k 的時候爲 1 ,否則爲 0 。直接尋找r_{nk}\mu_k 來最小化J 並不容易,不過我們可以採取迭代的辦法:先固定\mu_k ,選擇最優的r_{nk} ,很容易看出,只要將數據點歸類到離他最近的那個中心就能保證J 最小。下一步則固定r_{nk},再求最優的\mu_k。將J\mu_k 求導並令導數等於零,很容易得到J 最小的時候\mu_k 應該滿足:

亦即 \mu_k 的值應當是所有 cluster k 中的數據點的平均值。由於每一次迭代都是取到J 的最小值,因此J 只會不斷地減小(或者不變),而不會增加,這保證了 k-means 最終會到達一個極小值。雖然 k-means 並不能保證總是能得到全局最優解,但是對於這樣的問題,像 k-means 這種複雜度的算法,這樣的結果已經是很不錯的了。

下面我們來總結一下 k-means 算法的具體步驟:

  1. 選定 K 箇中心 \mu_k 的初值。這個過程通常是針對具體的問題有一些啓發式的選取方法,或者大多數情況下采用隨機選取的辦法。因爲前面說過 k-means 並不能保證全局最優,而是否能收斂到全局最優解其實和初值的選取有很大的關係,所以有時候我們會多次選取初值跑 k-means ,並取其中最好的一次結果。
  2. 將每個數據點歸類到離它最近的那個中心點所代表的 cluster 中。
  3. 用公式 \mu_k = \frac{1}{N_k}\sum_{j\in\text{cluster}_k}x_j 計算出每個 cluster 的新的中心點。
  4. 重複第二步,一直到迭代了最大的步數或者前後的 J 的值相差小於一個閾值爲止。

按照這個步驟寫一個 k-means 實現其實相當容易了,在 SciPy 或者 Matlab 中都已經包含了內置的 k-means 實現,不過爲了看看 k-means 每次迭代的具體效果,我們不妨自己來實現一下,代碼如下(需要安裝SciPymatplotlib) :

#This Python file uses the following encoding: UTF-8
from __future__ import with_statement
import  pickle 
from matplotlib import pyplot
from numpy import zeros, array, tile
from scipy.linalg import norm
#from numpy import norm
import numpy.matlib as ml
import random
 
def kmeans(X, k, observer=None, threshold=1e-15, maxiter=300):
    N = len(X)
    labels = zeros(N, dtype=int)
    centers = array(random.sample(list(X), k))
    print(centers)
    iter = 0
 
    def calc_J():  #計算消耗函數的值即所有點到與它對應的中心點距離的平方和
        sum = 0
        for i in range(N):
            sum += norm(X[i]-centers[labels[i]])  #返回X[i]-centers[labels[i]]矩陣的二範數
        return sum
 
    def distmat(X, Y):  #計算每個點到每個中心的距離
        n = len(X)
        m = len(Y)
        xx = ml.sum(X*X, axis=1)  #等於matlab中的sum(X.*X,2)即列相加
        yy = ml.sum(Y*Y, axis=1)
        xy = ml.dot(X, Y.T)       #等於matlab中的X*(Y')  
 
        return tile(xx, (m, 1)).T+tile(yy, (n, 1)) - 2*xy   #等於matlab中的repmat(a,m,n)
 
    Jprev = calc_J()
    while True:
        # notify the observer
        if observer is not None:
            observer(iter, labels, centers)
 
        # calculate distance from x to each center
        # distance_matrix is only available in scipy newer than 0.7
        # dist = distance_matrix(X, centers)
        dist = distmat(X, centers)
        # assign x to nearst center
        labels = dist.argmin(axis=1)  #n*m矩陣,每一行表示每一個點到三個中心點的距離
        # re-calculate each center
        for j in range(k):
            idx_j = (labels == j).nonzero()  #用labels和j比較選出所有相等的點
            centers[j] = X[idx_j].mean(axis=0)#對這些屬於j的點求平均值
 
        J = calc_J()
        iter += 1
 
        if Jprev-J < threshold:
            break
        Jprev = J
        if iter >= maxiter:
            break
 
    # final notification
    if observer is not None:
        observer(iter, labels, centers)
 
if __name__ == '__main__':
    # load previously generated points
    inf=open('g:\\data.pkl','rb')
    samples = pickle.load(inf)
        
    N = 0
    for smp in samples:
        N += len(smp[0])
    X = zeros((N, 2))
    idxfrm = 0
    for i in range(len(samples)):
        idxto = idxfrm + len(samples[i][0])
        X[idxfrm:idxto, 0] = samples[i][0]
        X[idxfrm:idxto, 1] = samples[i][1]
        idxfrm = idxto
 
    def observer(iter, labels, centers):
        print("iter %d." % iter)
        colors = array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
        pyplot.plot(hold=False)  # clear previous plot
        pyplot.hold(True)
 
        # draw points
        data_colors=[colors[lbl] for lbl in labels]
        pyplot.scatter(X[:, 0], X[:, 1], c=data_colors, alpha=0.5)
        # draw centers
        pyplot.scatter(centers[:, 0], centers[:, 1], s=200, c=colors)
 
        pyplot.savefig('g:\\kmeans_image\\iter_%02d.png' % iter, format='png')
 
    kmeans(X, 3, observer=observer)

需要注意的是上面代碼是我在Python3.2版本下編譯通過的,原作者用到了cpickle,而3.2版本沒有這個module,所以在數據方面我重新持久化了一份,點擊這裏下載

代碼有些長,不過因爲用 Python 來做這個事情確實不如 Matlab 方便,實際的 k-means 的代碼只是 41 到 47 行。首先 3 箇中心點被隨機初始化,所有的數據點都還沒有進行聚類,默認全部都標記爲紅色,如下圖所示:

iter_00

然後進入第一次迭代:按照初始的中心點位置爲每個數據點着上顏色,這是代碼中第 41 到 43 行所做的工作,然後 45 到 47 行重新計算 3 箇中心點,結果如下圖所示:

iter_01

可以看到,由於初始的中心點是隨機選的,這樣得出來的結果並不是很好,接下來是下一次迭代的結果:

iter_02

可以看到大致形狀已經出來了。再經過兩次迭代之後,基本上就收斂了,最終結果如下:

iter_04

不過正如前面所說的那樣 k-means 也並不是萬能的,雖然許多時候都能收斂到一個比較好的結果,但是也有運氣不好的時候會收斂到一個讓人不滿意的局部最優解,例如選用下面這幾個初始中心點:

iter_00_bad

最終會收斂到這樣的結果:

iter_03_bad

不得不承認這並不是很好的結果。不過其實大多數情況下 k-means 給出的結果都還是很令人滿意的,算是一種簡單高效應用廣泛的 clustering 方法。


 

發佈了82 篇原創文章 · 獲贊 3 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章