K-means 聚類算法及其代碼實現

序言

K-means算法是非監督學習(unsupervised learning)中最簡單也是最常用的一種聚類算法,具有的特點是:

  • 對初始化敏感。初始點選擇的不同,可能會產生不同的聚類結果
  • 最終會收斂。不管初始點如何選擇,最終都會收斂。

本文章介紹K-means聚類算法的思想,同時給出在matlab環境中實現K-means算法的代碼。代碼使用向量化(vectorization1)來計算,可能不是很直觀但是效率比使用循環算法高。

K-means算法

本節首先直觀敘述要解決的問題,然後給出所要求解的數學模型,最後從EM2 算法的角度分析K-means算法的特點。

問題描述

首先我們有N個數據D={x1,x2,...,xN} ,我們想把這些數據分成K個類。首先我們沒有任何的label 信息,所以這是一個unsupervied learning的問題。這個問題有一些難點,在於我們並不知道K 選擇多大時分類是合適的,另外由於這個問題對初始點的選擇是敏感的,我們也不好判斷怎麼樣的初始點是好的。所以,我們定義一個距離的概念,這個距離可以是很多種,例如就用最簡單的歐式距離 來作爲判斷標準,又因爲這裏對每個點,使用距離或者是距離的平方,其實並沒有什麼影響,所以爲了計算方便,我們就直接使用距離的平方2 作爲標準。我們想找到K 箇中心,數據離哪些中心近我們就將其定義爲哪一類,同時我們的K 箇中心能夠使這個分類最合理也就是每個點到其中心的距離的和最小。用語言描述爲

K 箇中心,數據屬於距離其最近的中心一類,這K 箇中心能使所有數據距離其中心的距離和最小。

爲了更好的理解,我將在下節給出一些數學符號來定義清楚問題。

問題定義

上小節我們知道要把數據分成K 個類別,就是要找出K 箇中心點,我們將這些K 箇中心點定義爲{μk}|Kk=1 . 同時,對於數據D={x1,x2,x3,...,xN} ,我們定義一個類別指示變量(set of binary indicator variables3{rnk|rnk{0,1}} ,表示xn(n(1,2,...,N)) 是否屬於第k 箇中心點的類,屬於就是1,不屬於就是0。因爲我們定義數據點屬於離他最近的中心點的類,所以rnk 的計算過程爲:

rnk=1,k=argminj||xnμj||0,otherwise.(1)

我們的目標就是要得到K 箇中心點,能夠使每個數據點到其中心點的距離(距離的平方)和最短,也就是讓目標函數

J=n=1Nk=1Krnk||xnμk||2(2)

最小。

問題求解

這一部分將介紹使用EM算法4來求解K-means問題。關於EM算法求解總體分爲兩種步驟

  • E(expectation): 求期望最大。初始化時,隨機生成K 箇中心點{μk}|Kk=1 。然後使用公式(1) 決定數據的類別。
  • M(Maximization): 這裏的極大化取決於你的問題,我們這裏是要最優化目標函數。所以在這一步我們保持數據的類別不變,要使用公式(2) 更新中心點,也就是要求出
    μk=argminukJ(3)

    這個等式。
    這裏我們注意到,因爲保持了類別不變,也就是說目標函數只有μk 一個變量,等式(3) 變成了
    μk=argminukJ(μk)

    式子。所以我們對目標函數求極值,也就對μk 求導並令其爲零,得到
    2n=1Nrnk(xnμk)=0

    這樣的式子。求解可以得到
    μk=nrnkxnnrnk(4)

    的表達式。
  • 重複以上兩步,直到收斂。

至此,我們就完成了對K-means方法的求解。接下來,我們將通過實例以及代碼實現來理解K-means。

K-means實現

這一節主要通過實例和代碼,來充分理解K-means算法,完成聚類分析,並在最後分析收斂效果。

實例分析

我們的數據來源是Old Faithful Geyser,我們想將其分成K 個類。但在處理之前需要對其進行歸一化,我對數據進行了標準歸一化,數據文件以及源代碼都已經放在我的github上面了。

代碼分析

都代碼還是先整體再局部吧。我們先對代碼整體設計如下

function [costDis] = runKMeans(K,fileString)
X=load(fileString);
%determine and store data set information
N=size(X,1);
D=size(X,2);
%allocate space for the K mu vectors
Kmus=zeros(K,D); % not need to allocate it but it is still worthy
%initialize cluster centers by randomly picking points from the data
rndinds=randperm(N);
Kmus=X(rndinds(1:K),:);
%specify the maximum number of iterations to allow
maxiters=1000;
for iter=1:maxiters
    %do this by first calculating a squared distance matrix where the n,k entry
    %contains the squared distance from the nth data vector to the kth mu vector
    %sqDmat will be an N-by-K matrix with the n,k entry as specfied above
    sqDmat=calcSqDistances(X,Kmus);
    %given the matrix of squared distances, determine the closest cluster
    %center for each data vector 
    Rnk=determineRnk(sqDmat);
    KmusOld=Kmus;
    plotCurrent(X,Rnk,Kmus);  
    pause(1);
    Kmus=recalcMus(X,Rnk);
    %check to see if the cluster centers have converged.  If so, break.
    if sum(abs(KmusOld(:)-Kmus(:)))<1e-6
        disp(iter);
        break
    end
end
costDis = sum(min(sqDmat,[],2));
end

首先讀入數據,XN×D 維的矩陣。然後初始化中心點KmusK×D 維度的矩陣。接下來進入循環,先使用函數calcSqDistances() 計算數據與各中心點之間的距離,然後determineRnk()根據距離決定數據屬於哪一類,然後recalcMus()根據確定好的數據的類重新計算出新的中心點,最後重複循環直到收斂。

接下來是各個內部函數,首先是距離計算函數。我們要得到的矩陣第n 行第k 列元素代表的是||xnμk||2 ,也就是

(X(n:,)-Kmus(k:,))*(X(n:,)-Kmus(k:,))'

這樣就能夠計算出一個元素的值,這裏面還要用到一點矩陣運算的技巧,因爲

||xnμk||2=(xnμk)(xnμk)T=xnxTn2xnμTk+μμT(5)

可以發現,其實對數據和中心點矩陣的每一行元素,只要計算自己與自己的距離,然後減去兩倍向量乘積的值就可以了。所以我們應該對每個矩陣先自己相乘得到自己的距離,比如對數據點這個距離就通過
Data_sq = diag(X*X');   % N by 1

來計算得出。
計算距離的代碼爲

function SQD = calcSqDistances(X,Kmus)
% compute the squared distance w.r.t. each center point for every data
% X; N by D; Kmus: K by D
% ||x-u||^2 = xx' - 2xu' + uu'  N by K
N = size(X,1);
D = size(X,2);
K = size(Kmus,1);
Data_sq = diag(X*X');   % N by 1
Kmus_sq = diag(Kmus*Kmus');    % 1 by K
trans = 2*X*Kmus';  % N by K
SQD = repmat(Data_sq,1,K) - trans + repmat(Kmus_sq',N,1);
end

決定類的函數,其實通過公式(1) 已經很容易理解了,直接放代碼了

function RnkMat = determineRnk(sqDmat)
% calculate the label for each cluster
% 1 for belong, 0 for not belong
N = size(sqDmat,1);
K = size(sqDmat,2);
RnkMat = zeros(N,K);
[~,minIndex] = min(sqDmat,[],2);
positionVec = 1:N;
idxVec = N*(minIndex-1) + positionVec';     % or we can ues this
% idxVec = sub2ind([N,K],positionVec',minIndex); but it is slower than my
% code implementation
RnkMat(idxVec) = 1;
end

最後就是更新中心點的函數,也是根據EM算法中的公式(4) 就可以得到了。

function Kmus = recalcMus(X,Rnk)
% get the Kmus from the mean value of the cluster
% mu_k = frac{\sum_n{r_{nk}X_n}{\sum_n{r_{nk}}}
% X: N by D
% Rnk: N by K
% Kmus: K by D
N = size(X,1);
K = size(Rnk,2);
D = size(X,2);
sumCluster = Rnk'*X;    % K by D
numCluster = sum(Rnk)';  % K by 1
normMat = repmat(numCluster,1,D);
Kmus = sumCluster./normMat;
end

最後是一個小trick在主程序畫圖過程中的plotCurrent() 函數後面跟着一個停頓函數pause(1) 會在循環過程中產生動態效果,如下圖所示(忽略噁心的水印)

k-means

繪圖函數是這樣的

function    plotCurrent(X,Rnk,Kmus)
[N,D]=size(X);
K=size(Kmus,1);
clf;
figure(1);
hold on;
InitColorMat= [1 0 0;   
               0 1 0;   
               0 0 1;
               0 0 0;
               1 1 0; 
               1 0 1; 
               0 1 1;
               0.5 1 0.5];
KColorMat=InitColorMat(1:K,:);
colorVec=Rnk*KColorMat;
muColorVec=eye(K)*KColorMat;
scatter(X(:,1),X(:,2),[],colorVec)
scatter(Kmus(:,1),Kmus(:,2),200,muColorVec,'d','filled');
axis equal;
hold off;
end

結果分析

隨着K的變化,整體的距離變化爲如圖所示,動態變化上圖已經展示。


調試代碼爲

% KMeans_script
% for i = 1:100
filename = 'scaledfaithful.txt';
%%
K = 2;
k2_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k2_cost = runKMeans(K,filename);
    k2_cost_all = k2_cost_all + k2_cost;
end
k2_cost_avg = k2_cost_all/max_num;
%%
K = 3;
k3_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k3_cost = runKMeans(K,filename);
    k3_cost_all = k3_cost_all + k3_cost;
end
k3_cost_avg = k3_cost_all/max_num;
%%
K = 4;
k4_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k4_cost = runKMeans(K,filename);
    k4_cost_all = k4_cost_all + k4_cost;
end
k4_cost_avg = k4_cost_all/max_num;
%%
K = 5;
k5_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k5_cost = runKMeans(K,filename);
    k5_cost_all = k5_cost_all + k5_cost;
end
k5_cost_avg = k5_cost_all/max_num;
%%
K = 6;
k6_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k6_cost = runKMeans(K,filename);
    k6_cost_all = k6_cost_all + k6_cost;
end
k6_cost_avg = k6_cost_all/max_num;
%%
K = 7;
k7_cost_all = 0;
max_num = 100;
for num_comput = 1:max_num
    k7_cost = runKMeans(K,filename);
    k7_cost_all = k7_cost_all + k7_cost;
end
k7_cost_avg = k7_cost_all/max_num;
%%
cost_K = [k2_cost_avg, k3_cost_avg, k4_cost_avg, k5_cost_avg, k6_cost_avg, k7_cost_avg];
K = 2:7;
plot(K, cost_K, 'rx-','LineWidth', 3)
xlim([2 7])
ylim([20 100])
xlabel('K')
ylabel('cost function value')

雖然無法找到一個最優的K值,但相對來說,k=4或5的時候效果還是不錯的。當K=4的時候,收斂圖爲


可以發現,收斂的還是非常快的。

總結

本文介紹了聚類算法中常用的K-means算法。從EM算法求解K-means算法問題,並給出了matlab下實現K-means的算法程序。所有的程序和數據均可以從我的github上面下載。希望對大家有所幫助!


參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章