機器學習——詳解經典聚類演算法Kmeans
今天是機器學習專題的第12篇文章,我們一起來看下Kmeans聚類演算法。
在上一篇文章當中我們討論了KNN演算法,KNN演算法非常形象,通過距離公式找到最近的K個鄰居,通過鄰居的結果來推測當前的結果。今天我們要來看的演算法同樣非常直觀,也是最經典的聚類演算法之一,它就是Kmeans。
我們都知道,在英文當中Means是平均的意思,所以也有將它翻譯成K-均值演算法的。當然,含義是一樣的,都是通過求均值的方式來獲取樣本的類簇。
既然知道Kmeans演算法和均值和類簇有關,那麼剩下的問題就只有兩個:首先,我們應該怎麼來計算均值,其次當我們獲取了均值之後,又是怎麼來聚類的呢?
聚類演算法
上面的兩個問題我們先放一放,我們先來看一個例子,假設我們有一系列使用者的收入樣本,我們想要將這批使用者根據他們的收入、居住地以及消費情況分成富人階級、中產階級和工薪階級。
在這個問題當中,我們只知道我們希望把樣本分成三類,但是怎麼來分,我們並不清楚,這是我們希望模型替我們完成的。也就是說我們希望模型能夠自動識別這些樣本之間的關聯性,把關聯性強的樣本聚在一起,成為一個類簇。在這個問題當中,我們希望模型替我們把資料分成三個類別。
如果讓我們人工來劃分這個問題當然很簡單,我們直接根據這些使用者的收入來分。直接將使用者的收入畫一個折線圖,然後來尋找最佳的兩個切分點,三下五除二很快就搞定了。但如果我們的特徵當中沒有使用者的收入呢?如果我們能知道使用者有沒有車,有沒有房,家裡的存款和所有外債,這就沒那麼直觀了,不過也容易,我們簡單地建模也容易解決。再如果我們連車房的資訊都沒有,只能拿到使用者在哪裡上班,使用者住在哪裡呢?這個問題是不是就更抽象了?
在特徵比較抽象和隱晦的時候,我們想直接劃分往往不太容易,由於不知道真實的標籤,我們也沒有辦法用上監督模型。為了解決問題,Kmeans只能反其道而行之,我們不再對資料進行劃分了,而讓比較接近的資料自己聚集在一起。Kmeans演算法正是基於這一思想而生,讓資料通過某種演算法聚集,不再進行劃分的方法稱為聚類演算法。
在聚類問題當中,一系列樣本被模型根據資料的屬性聚合在了一起,成為了同一個類別。這裡的類別就稱為這些樣本的類簇(cluster)。每一個簇的中心點稱為簇中心。所以,KMeans演算法,顧名思義,就是將樣本根據使用者設定的K值,一共聚類成K個類簇。
Kmeans原理
不知道大家有沒有聽說過這麼一個理論,人類和計算機其實是相反的。一些對於人類來說困難的問題,對於計算機非常簡單。比如記憶,人類很難瞬間記憶大量的東西,而計算機不是,只要頻寬和容量足夠,再多的資料都能記住。不但能記住,而且絕不會出錯。再比如計算,人類很難快速計算複雜的公式,基本上兩位數以上的乘除就必須要藉助工具了。但計算機不是,只要CPU資源足夠,再大量的計算都可以進行。
但是呢,人類覺得很簡單的東西,對計算機來說非常困難。比如視覺,我們人類可以很輕易地分辨圖片上的貓和狗,但是計算機不行。即使是深度學習和AI大行其道的今天,我們也要專門設計複雜的模型和大量資料進行訓練才能讓計算機學會分辨圖片的內容。再比如創作,人類可以創作出前人沒有的東西,計算機則不能,所謂的計算機譜曲、寫作只不過是程式按照固定的模式加上一些隨機波動的值綜合作用的結果而已。再比如思考,人類可以思考之前從未見過的問題,計算機顯然不能。
比如上圖,我們人類一眼看去這是三個類別,但是計算機不行。資料在計算機當中是離散的,計算機也沒有視覺,看不到資料之間的聯絡。所以我們看著簡單的問題,其實並沒有那麼簡單,但其實剛才的分析當中我們已經道出了本質:既然計算機看不到聯絡,那麼我們就要想辦法讓它能夠“看到”,說看到應該不夠準確,準確地說是算到。
回想一下,我們剛才是怎麼快速分辨出圖上有三個類別的?你會說很簡單嘛,因為三個區域內點最多啊。這個說法很正確,但是不夠量化,如果我們量化一下,應該是存在三個區域密度最大。一旦量化表達以後,問題就清楚了,我們正是要通過密度來進行聚類。Kmeans正是基於這一樸素的思想,但是它過於樸素,並沒有設計計算類簇數量的演算法,所以這個類別數量K,是要使用者提供的。
也就是說演算法並不知道要聚成幾類,我們說是幾類就是幾類。
我們忽略這一細節,假設我們通過某種奇怪的方法知道了資料一共分成三類,那麼Kmeans怎麼進行劃分呢?
我們深入思考會發現我們雖然說是要量化密度,但是密度很難量化。因為密度的定義本身就是基於聚類之後的結果的,我們肯定是已經知道了這樣一批資料聚集在了一起才能算它們的密度,而不是相反。所以這個思路是靠譜的,但是直接這麼做是不行的。但是直接做不行,不意味著倒著不可以,這個思路在數學上很常見,在這裡我們又遇到了。
既然我們通過密度來聚類不行,那麼我們能不能先聚類再算密度,根據密度的結果調整呢?
我Google了好久也沒找到Kmeans原作者的資訊,但我想能想出這麼天才想法的人,他一定很機智。Kmeans正是基於這麼樸素又機智的思路衍生的。
初始化
在演算法執行的伊始,Kmeans會在資料集的範圍當中隨機選擇K箇中心點,然後依據這K箇中心點進行聚類。中心點有了聚類其實很容易,對於每一個樣本來說我們只需要計算一下它和所有中心的距離,選擇最近的那個就好了。
當然,這樣得到的結果肯定很不準,但是沒關係,即使依據不靠譜的中心,我們也可以完成聚類,我們把隨機到的中心點的位置和最後的聚類結果都畫在一張圖上,可以看到雖然一開始選的位置看起來不是那麼靠譜,但是我們一樣可以達成一個不錯的結果。
初始的聚類結果肯定是不準的,但是沒有關係,我們不怕不準,就怕沒有結果。有了結果就好辦了,我們可以針對這個結果進行分析來檢視優化的方向。有了優化的方向就可以讓結果變得越來越準,就好像線上性迴歸當中,我們也不是第一下就搞定最佳引數的選值的,也是通過梯度下降一點一點迭代出來的。
迭代
在我們介紹具體的迭代方法之前,先來分析下情況。顯然由於隨機選取的關係,聚類的結果肯定是不準的,不準的原因是由於我們隨機選取的中心和類簇距離太遠導致的。也就是說我們要想辦法讓中心向著類簇靠近。
那怎麼才能靠近呢,我們先來看一下完美聚類之後的情況。
我們來觀察一下,完美聚類時中心點和類簇重疊。那麼這個中心點有什麼性質呢?如果對物理熟悉的話,應該能聯想到,這個中心點應該是這個類樣本的質心。即使不熟悉這個概念也沒關係,我們通過上圖可以觀察出來,樣本點均勻地分散在中心的四周。均勻地分散會有一個什麼特點?也容易想到,就是出現在中心點左側和右側,上側和下側,以及其他各個方向上的點數量和分佈都差不多。我們量化一下這個概念,可以得到類別當中所有點的座標均值就是中心點的位置。
那麼問題來了,在一個聚類錯誤的情況下,樣本座標的均值(即質心)和我們選取的中心點會重合嗎?如果不重合會有怎樣的偏差呢?
我們從上圖其實可以猜出來,由於我們選的中心點位置不對,所以它和聚類之後樣本的質心肯定是不重合的。兩者偏差的方向,就是它距離質心的方向。
這個結論也很樸素,因為距離真實的類簇越近,點越密集,那麼算出來的質心顯然會更靠近真實類簇的方向。有了這個結論,就很簡單了,我們只要每次聚類之後計算一下各個類的質心,然後將算出來的質心作為下一次聚類的中心點重新聚類,一直重複上面的過程就行了。當聚類之前的中心和聚類之後的質心重疊的時候,就說明聚類收斂,我們找到了類簇。
下圖展現了一個類別中心隨著迭代而變化的情況,我們可以很直觀地看到,隨著我們的迭代,我們的類中心距離真正的簇中心越來越近,經過了三次迭代,就已經非常接近最後的結果了。所以這個結論是正確的,用質心來作為新的中心來迭代的思路是可行的。
程式碼實現
Kmeans的原理以及牽引侯貴搞清楚了之後,用Python實現就變得很簡單了。
我們當然可以自己編寫生成資料的邏輯,但sklearn庫當中為我們提供了創造資料的API,通過呼叫API我們可以很輕鬆地創造我們想要的資料。我們可以使用dataset.make_blobs創造聚類資料。傳入樣本的數量和特徵的數量,真實類簇的座標以及樣本的標準差,就可以得到一批相應的樣本。
def createData():
X, y = datasets.make_blobs(n_samples=1000, n_features=2, centers=[[-1,-1],[1,1],[2,2]], cluster_std=[0.2,0.3,0.4])
return X, y
建立完資料之後,下面我們就可以開始演算法的實現了。
首先,我們先開發整個演算法的基礎方法,來簡化後續的開發。在KMeans問題當中,我們已經知道我們是通過向量和各類簇中心在樣本空間的距離來調整樣本的所屬類別。所以,我們先開發向量之間距離的計算方法。
使用numpy,整個的計算過程會變得非常簡單:
def calculateDistance(vecA, vecB):
return np.sqrt(np.sum(np.square(vecA - vecB)))
在這一行程式碼當中,我們先計算了兩個向量的差向量。然後我們對這個差向量的每一項求平方和再開方,這樣就得到了向量A和B的歐氏距離。
接著,我們需要隨機K個類簇的中心點的座標。雖然在KMeans演算法當中類簇的選擇是隨機的,但是需要注意的是,我們的隨機的範圍並不是無限的。因為聚類是為了尋找樣本密集度最高的K個位置,沒有樣本分佈的地方自然也是不可能找到合法的類簇的。所以我們可以將隨機的範圍限制在樣本的分佈範圍內,這樣可以大大簡化計算量。
def randomCenter(samples, K):
m,n = np.shape(samples)
centers = np.mat(np.zeros((K, n)))
for i in range(n):
# 通過np.max獲取i列最大值
mxi = np.max(samples[:, i])
# 通過np.min獲取i列最小值
mni = np.min(samples[:, i])
rangeI = mxi - mni
# 為簇中心第i列賦值
centers[:, i] = np.mat(mni + rangeI * np.random.rand(K, 1))
return centers
上面的邏輯不難理解,我們首先為K個簇中心建立座標矩陣並初始化為0,這裡的n是樣本的維度數。接著,我們遍歷這n個維度,查詢樣本當中每個維度的最大值和最小值。有了這兩個值,我們就知道了簇中心在每個樣本維度上的取值範圍。最後,我們再呼叫random.rand方法隨機出具體的座標即可。
到這裡,演算法需要的兩個基本工具都已經開發完了。接下來只要實現迭代的流程,整個KMeans就算是完成了。
在我們繼續往下開發之前,我們先來測試一下我們開發好的這兩個介面。
首先,我們先生成資料:
看到有資料產出,說明我們的資料已經生成好了,接下來根據生成的資料,隨機選出K個簇中心。
我們在生成資料的時候傳入的樣本中心點有三個,所以簇中心數量就是3,也就是說我們的K就是3,那麼我們接著呼叫randomCenter方法,檢視結果。
果然,我們生成了3個點。為了保險,我們需要輸出樣本的範圍,檢查我們生成的點的座標是否在我們樣本的範圍當中。
使用numpy的max和min方法,結合Python語言的切片操作,我們可以非常方便地求解這四個值。很明顯,我們的簇中心都在範圍當中。我們的程式碼沒有問題。
這兩個方法沒問題之後,我們就可以著手開發KMeans的核心邏輯了,也就是聚類的計算邏輯。
根據我們之前列出來的虛擬碼,我們先隨機出簇中心。然後根據簇中心給各個樣本標記上類別。最後再根據標記好的樣本更新簇中心的位置,整個邏輯其實非常簡單,寫成程式碼也不復雜:
def KMeans(dataset, k):
m, n = np.shape(dataset)
# 最後的返回結果,一共兩維,第一維是所屬類別,第二維是到簇中心的距離
clusterPos = np.zeros((m, 2))
centers = randCenter(dataset, k)
clusterChange = True
while clusterChange:
clusterChange = False
# 遍歷所有樣本
for i in range(m):
minD = inf
idx = -1
# 遍歷到各個簇中心的距離
for j in range(k):
dis = calculateDistance(centers[j,:], dataset[i, :])
if dis < minD:
minD = dis
idx = j
# 如果所屬類別發生變化
if clusterPos[i,0] != idx:
clusterChange = True
# 更新樣本聚類結果
clusterPos[i,:] = idx, minD
# 更新簇中心的座標
for i in range(k):
nxtClust = dataset[np.nonzero(clusterPos[:,0] == i)[0]]
centers[i,:] = np.mean(nxtClust, axis=0)
return centers, clusterPos
下面,我們來測試一下我們的程式碼,看看能不能聚類出正確的結果。
centers, clusterRet = KMeans(x, 3)
plt.scatter(x[:,0],x[:,1],c=clusterRet[:,0] ,s=3,marker='o')
plt.scatter(centers[:, 0].A, centers[:, 1].A, c='red', s=100, marker='x')
plt.show()
我們把樣本當中的所有點根據聚類之後的結果進行繪製,再在同一張圖上標記出簇中心的位置,得到的結果如下。
不難看出,在上圖當中,無論是簇中心的位置還是最後的聚類結果,基本上和我們人工估計的結果一樣。說明我們寫的KMeans演算法成功執行,並輸出了正確的結果。
總結
到這裡,關於Kmeans演算法的原理和程式碼就都介紹完了。不知道大家有什麼感覺,我當時初學這個演算法的時候,最大的感受就是簡單,這個演算法也太“兒戲”了,理解起來也很容易,沒有什麼彎彎繞或者是複雜的東西,所有問題和思路都直來直去。
演算法簡單我們學習起來就容易,但是往往太簡單的演算法都會留下短板。Kmeans的短板也很明顯,相信大家也都感受到了。我們每次迭代的時候,都需要對所有的樣本計算所屬的類別,這可是一次全量的計算。而由於我們初始的中心點是隨機選取的,這也導致了一開始中心的位置和最後的類簇可能相去甚遠,距離越遠顯然需要的迭代次數也就越多,那麼帶來的計算消耗自然也就越大。
那麼,針對kmeans效率的問題有沒有什麼提升的方法呢?
大家可以先思考一下這個問題,我將會在下週的機器學習專題當中和大家討論相關內容。
同樣,由於Kmeans演算法原理簡單,實現容易,所以它經常出現在各大公司的招聘筆試題當中。據我所知,阿里巴巴有好幾年的筆試題就是讓選手手寫一個kmeans聚類。所以雖然這個演算法簡單,但是我們也不能掉以輕心。另外,對於演算法也不能滿足於瞭解原理,凡事可以多想一想多問一問,這樣理解才更加深入,以後應對面試才更加靈活。
今天的文章就是這些,如果覺得有所收穫,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。