轉:http://blog.pluskid.org/?p=39python
很久沒有寫 blog 了,一來是 blog 下線一段時間,而租 DreamHost 的事情又一直沒弄好;二來是沒有太多時間,每天都跑去實驗室。如今主要折騰 Machine Learning 相關的東西,由於不少東西都不懂,因此平時也找一些資料來看。按照我之前的更新速度的話,這麼長時間不寫 blog 確定是要被悶壞的,因此我也以爲仍是不按期地整理一下本身瞭解到的東西,放在 blog 上,一來梳理老是有助於加深理解的,二來也算共享一下知識了。那麼,仍是從 clustering 提及吧。算法
Clustering 中文翻譯做「聚類」,簡單地說就是把類似的東西分到一組,同 Classification (分類)不一樣,對於一個 classifier ,一般須要你告訴它「這個東西被分爲某某類」這樣一些例子,理想狀況下,一個 classifier 會從它獲得的訓練集中進行「學習」,從而具有對未知數據進行分類的能力,這種提供訓練數據的過程一般叫作 supervised learning (監督學習),而在聚類的時候,咱們並不關心某一類是什麼,咱們須要實現的目標只是把類似的東西聚到一塊兒,所以,一個聚類算法一般只須要知道如何計算類似 度就能夠開始工做了,所以 clustering 一般並不須要使用訓練數據進行學習,這在 Machine Learning 中被稱做 unsupervised learning (無監督學習)。dom
舉一個簡單的例子:如今有一羣小學生,你要把他們分紅幾組,讓組內的成員之間儘可能類似一些,而組之間則差異大一些。最後分出怎樣的結果,就取決於你對於「類似」的定義了,好比,你決定男生和男生是類似的,女生和女生也是類似的,而男生和女生之間則差異很大」,這樣,你其實是用一個可能取兩個值「男」和「女」的離散變量來表明了原來的一個小學生,咱們一般把這樣的變量叫作「特徵」。實際上,在這種狀況下,全部的小學生都被映射到了兩個點的其中一個上,已經很天然地造成了兩個組,不須要專門再作聚類了。另外一種多是使用「身高」這個特徵。我在讀小學候,每週五在操場開會訓話的時候會按照你們住的地方的地域和距離遠近來列隊,這樣結束以後就能夠結隊回家了。除了讓事物映射到一個單獨的特徵以外,一種常見的作法是同時提取 N 種特徵,將它們放在一塊兒組成一個 N 維向量,從而獲得一個從原始數據集合到 N 維向量空間的映射——你老是須要顯式地或者隱式地完成這樣一個過程,由於許多機器學習的算法都須要工做在一個向量空間中。機器學習
那麼讓咱們再回到 clustering 的問題上,暫且拋開原始數據是什麼形式,假設咱們已經將其映射到了一個歐幾里德空間上,爲了方便展現,就使用二維空間吧,以下圖所示:函數
從數據點的大體形狀能夠看出它們大體聚爲三個 cluster ,其中兩個緊湊一些,剩下那個鬆散一些。咱們的目的是爲這些數據分組,以便能區分出屬於不一樣的簇的數據,若是按照分組給它們標上不一樣的顏色,就是這個樣子:學習
那麼計算機要如何來完成這個任務呢?固然,計算機尚未高級到可以「經過形狀大體看出來」,不過,對於這樣的 N 維歐氏空間中的點進行聚類,有一個很是簡單的經典算法,也就是本文標題中提到的 k-means 。在介紹 k-means 的具體步驟以前,讓咱們先來看看它對於須要進行聚類的數據的一個基本假設吧:對於每個 cluster ,咱們能夠選出一箇中心點 (center) ,使得該 cluster 中的全部的點到該中心點的距離小於到其餘 cluster 的中心的距離。雖然實際狀況中獲得的數據並不能保證老是知足這樣的約束,但這一般已是咱們所能達到的最好的結果,而那些偏差一般是固有存在的或者問題自己的不可分性形成的。例以下圖所示的兩個高斯分佈,從兩個分佈中隨機地抽取一些數據點出來,混雜到一塊兒,如今要讓你將這些混雜在一塊兒的數據點按照它們被生成的那個分佈分開來:優化
因爲這兩個分佈自己有很大一部分重疊在一塊兒了,例如,對於數據點 2.5 來講,它由兩個分佈產生的機率都是相等的,你所作的只能是一個猜想;稍微好一點的狀況是 2 ,一般咱們會將它歸類爲左邊的那個分佈,由於機率大一些,然而此時它由右邊的分佈生成的機率仍然是比較大的,咱們仍然有不小的概率會猜錯。而整個陰影部分是咱們所能達到的最小的猜錯的機率,這來自於問題自己的不可分性,沒法避免。所以,咱們將 k-means 所依賴的這個假設看做是合理的。spa
基於這樣一個假設,咱們再來導出 k-means 所要優化的目標函數:設咱們一共有 N 個數據點須要分爲 K 個 cluster ,k-means 要作的就是最小化.net
這個函數,其中 在數據點 n 被歸類到 cluster k 的時候爲 1 ,不然爲 0 。直接尋找
和
來最小化
並不容易,不過咱們能夠採起迭代的辦法:先固定
,選擇最優的
,很容易看出,只要將數據點歸類到離他最近的那個中心就能保證
最小。下一步則固定
,再求最優的
。將
對
求導並令導數等於零,很容易獲得
最小的時候
應該知足:翻譯
亦即 的值應當是全部 cluster k 中的數據點的平均值。因爲每一次迭代都是取到
的最小值,所以
只會不斷地減少(或者不變),而不會增長,這保證了 k-means 最終會到達一個極小值。雖然 k-means 並不能保證老是能獲得全局最優解,可是對於這樣的問題,像 k-means 這種複雜度的算法,這樣的結果已是很不錯的了。
下面咱們來總結一下 k-means 算法的具體步驟:
按照這個步驟寫一個 k-means 實現其實至關容易了,在 SciPy 或者 Matlab 中都已經包含了內置的 k-means 實現,不過爲了看看 k-means 每次迭代的具體效果,咱們不妨本身來實現一下,代碼以下(須要安裝 SciPy 和 matplotlib) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
#!/usr/bin/python from __future__ import with_statement import cPickle as pickle from matplotlib import pyplot from numpy import zeros, array, tile from scipy.linalg 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(X, k)) iter = 0 def calc_J(): sum = 0 for i in xrange(N): sum += norm(X[i]-centers[labels[i]]) return sum def distmat(X, Y): n = len(X) m = len(Y) xx = ml.sum(X*X, axis=1) yy = ml.sum(Y*Y, axis=1) xy = ml.dot(X, Y.T) return tile(xx, (m, 1)).T+tile(yy, (n, 1)) - 2*xy 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) # re-calculate each center for j in range(k): idx_j = (labels == j).nonzero() centers[j] = X[idx_j].mean(axis=0) 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 with open('cluster.pkl') as inf: 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('kmeans/iter_%02d.png' % iter, format='png') kmeans(X, 3, observer=observer) |
代碼有些長,不過由於用 Python 來作這個事情確實不如 Matlab 方便,實際的 k-means 的代碼只是 41 到 47 行。首先 3 箇中心點被隨機初始化,全部的數據點都尚未進行聚類,默認所有都標記爲紅色,以下圖所示:
而後進入第一次迭代:按照初始的中心點位置爲每一個數據點着上顏色,這是代碼中第 41 到 43 行所作的工做,而後 45 到 47 行從新計算 3 箇中心點,結果以下圖所示:
能夠看到,因爲初始的中心點是隨機選的,這樣得出來的結果並非很好,接下來是下一次迭代的結果:
能夠看到大體形狀已經出來了。再通過兩次迭代以後,基本上就收斂了,最終結果以下:
不過正如前面所說的那樣 k-means 也並非萬能的,雖然許多時候都能收斂到一個比較好的結果,可是也有運氣很差的時候會收斂到一個讓人不滿意的局部最優解,例如選用下面這幾個初始中心點:
最終會收斂到這樣的結果:
不得不認可這並非很好的結果。不過其實大多數狀況下 k-means 給出的結果都仍是很使人滿意的,算是一種簡單高效應用普遍的 clustering 方法。
Update 2010.04.25: 不少人都問我要 cluster.pkl ,我乾脆把它上傳上來吧,實際上是很容易本身生成的,點擊這裏下載。