K-means算法是非监督学习(unsupervised learning)中最简单也是最常用的一种聚类算法,具有的特点是:
本文章介绍K-means聚类算法的思想,同时给出在matlab环境中实现K-means算法的代码。代码使用向量化(vectorization1)来计算,可能不是很直观但是效率比使用循环算法高。
本节首先直观叙述要解决的问题,然后给出所要求解的数学模型,最后从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 的计算过程为:
我们的目标就是要得到 K 个中心点,能够使每个数据点到其中心点的距离(距离的平方)和最短,也就是让目标函数
这一部分将介绍使用EM算法4来求解K-means问题。关于EM算法求解总体分为两种步骤
至此,我们就完成了对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
首先读入数据,X
为 N×D 维的矩阵。然后初始化中心点Kmus
为 K×D 维度的矩阵。接下来进入循环,先使用函数calcSqDistances()
计算数据与各中心点之间的距离,然后determineRnk()
根据距离决定数据属于哪一类,然后recalcMus()
根据确定好的数据的类重新计算出新的中心点,最后重复循环直到收敛。
接下来是各个内部函数,首先是距离计算函数。我们要得到的矩阵第 n 行第 k 列元素代表的是 ||xn−μk||2 ,也就是
(X(n:,)-Kmus(k:,))*(X(n:,)-Kmus(k:,))'
这样就能够计算出一个元素的值,这里面还要用到一点矩阵运算的技巧,因为
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)
会在循环过程中产生动态效果,如下图所示(忽略恶心的水印)
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上面下载。希望对大家有所帮助!