K-means算法的实现原理和分析

时间:2022-12-07 18:52:46

一、概述

K-means算法是最为经典的基于划分的聚类方法,是十大经典数据挖掘算法之一。K-means算法的基本思想是:以空间中k个点为中心进行聚类,对最靠近他们的对象归类。通过迭代的方法,逐次更新各聚类中心的值,直至得到最好的聚类结果。
k-means 算法接受参数 k ,然后将事先输入的n个数据对象划分为 k个聚类以便使得所获得的聚类满足,同一聚类中的对象相似度较高,而不同聚类中的对象相似度较小。聚类相似度是利用各聚类中对象的均值所获得一个“中心对象”(引力中心)来进行计算的。

二、实现原理

KMeans算法的基本思想是初始随机给定K个簇中心,按照最邻近原则把待分类样本点分到各个簇。然后按平均法重新计算各个簇的质心,从而确定新的簇心。一直迭代,直到簇心的移动距离小于某个给定的值。
K-Means聚类算法主要分为三个步骤:
(1)第一步是为待聚类的点寻找聚类中心;
(2)第二步是计算每个点到聚类中心的距离,将每个点聚类到离该点最近的聚类中去;
(3)第三步是计算每个聚类中所有点的坐标平均值,并将这个平均值作为新的聚类中心;
反复执行(2)、(3),直到聚类中心不再进行大范围移动或者聚类次数达到要求为止。

下图展示了对n个样本点进行K-means聚类的效果,这里k取2:

K-means算法的实现原理和分析

(a)未聚类的初始点集;

(b)随机选取两个点作为聚类中心;

(c)计算每个点到聚类中心的距离,并聚类到离该点最近的聚类中去;

(d)计算每个聚类中所有点的坐标平均值,并将这个平均值作为新的聚类中心;

(e)重复(c),计算每个点到聚类中心的距离,并聚类到离该点最近的聚类中去;

(f)重复(d),计算每个聚类中所有点的坐标平均值,并将这个平均值作为新的聚类中心。

该算法的最大优势在于简洁和快速。算法的关键在于初始中心的选择和距离公式。

下面给出其Java实现:


class point {
public float x = 0;

public float y = 0;

public int flage = -1;

public float getX() {
return x;
}

public void setX(float x) {
this.x = x;
}

public float getY() {
return y;
}

public void setY(float y) {
this.y = y;
}
}

public class Kcluster {

point[] ypo;// 点集

point[] pacore = null;// old聚类中心

point[] pacoren = null;// new聚类中心

// 初试聚类中心,点集
public void productpoint() {
Scanner cina = new Scanner(System.in);
System.out.print("请输入聚类中点的个数(随机产生):");
int num = cina.nextInt();

ypo = new point[num];
// 随机产生点
for (int i = 0; i < num; i++) {

float x = (int) (new Random().nextInt(10));
float y = (int) (new Random().nextInt(10));

ypo[i] = new point();// 对象创建
ypo[i].setX(x);
ypo[i].setY(y);

}

// 初始化聚类中心位置
System.out.print("请输入初始化聚类中心个数(随机产生):");
int core = cina.nextInt();
this.pacore = new point[core];// 存放聚类中心
this.pacoren = new point[core];

Random rand = new Random();
int temp[] = new int[core];
temp[0] = rand.nextInt(num);
pacore[0] = new point();
pacore[0].x = ypo[temp[0]].x;
pacore[0].y = ypo[temp[0]].y;
pacore[0].flage = 0;
// 避免产生重复的中心
for (int i = 1; i < core; i++) {
int flage = 0;
int thistemp = rand.nextInt(num);
for (int j = 0; j < i; j++) {
if (temp[j] == thistemp) {
flage = 1;// 有重复
break;

}
}
if (flage == 1) {
i--;
} else {
pacore[i] = new point();
pacore[i].x = ypo[thistemp].x;
pacore[i].y = ypo[thistemp].y;
pacore[i].flage = 0;// 0表示聚类中心
}

}
System.out.println("初始聚类中心:");
for (int i = 0; i < pacore.length; i++) {
System.out.println(pacore[i].x + " " + pacore[i].y);
}

}

// ///找出每个点属于哪个聚类中心
public void searchbelong()// 找出每个点属于哪个聚类中心
{

for (int i = 0; i < ypo.length; i++) {
double dist = 999;
int lable = -1;
for (int j = 0; j < pacore.length; j++) {

double distance = distpoint(ypo[i], pacore[j]);
if (distance < dist) {
dist = distance;
lable = j;
// po[i].flage = j + 1;// 1,2,3......

}
}
ypo[i].flage = lable + 1;

}

}

// 更新聚类中心
public void calaverage() {

for (int i = 0; i < pacore.length; i++) {
System.out.println("以<" + pacore[i].x + "," + pacore[i].y + ">为中心的点:");
int numc = 0;
point newcore = new point();
for (int j = 0; j < ypo.length; j++) {

if (ypo[j].flage == (i + 1)) {
System.out.println(ypo[j].x + "," + ypo[j].y);
numc += 1;
newcore.x += ypo[j].x;
newcore.y += ypo[j].y;

}
}
// 新的聚类中心(就是所有聚合点的中心)
pacoren[i] = new point();
pacoren[i].x = newcore.x / numc;//所有聚类元素x坐标的和/元素数
pacoren[i].y = newcore.y / numc;
pacoren[i].flage = 0;
System.out.println("新的聚类中心:" + pacoren[i].x + "," + pacoren[i].y);

}
}

public double distpoint(point px, point py) {

return Math.sqrt(Math.pow((px.x - py.x), 2) + Math.pow((px.y - py.y), 2));

}

public void change_oldtonew(point[] old, point[] news) {
for (int i = 0; i < old.length; i++) {
old[i].x = news[i].x;
old[i].y = news[i].y;
old[i].flage = 0;// 表示为聚类中心的标志。
}
}

public void movecore() {
// this.productpoint();//初始化,样本集,聚类中心,
this.searchbelong();
this.calaverage();//
double movedistance = 0;
int biao = -1;// 标志,聚类中心点的移动是否符合最小距离
for (int i = 0; i < pacore.length; i++) {
movedistance = distpoint(pacore[i], pacoren[i]);//计算新旧两个中心点的距离
System.out.println("distcore:" + movedistance);// 聚类中心的移动距离
if (movedistance < 0.01) {
biao = 0;

} else {

biao = 1;// 需要继续迭代,
break;

}
}
if (biao == 0) {
System.out.print("迭代完毕!!!!!");
} else {
change_oldtonew(pacore, pacoren);
movecore();
}

}

public static void main(String[] args) {
// TODO Auto-generated method stub

Kcluster kmean = new Kcluster();
kmean.productpoint();
kmean.movecore();
}

}

程序运行结果如下:

K-means算法的实现原理和分析
详细信息如下:(每次运行结果会有所不同)

请输入聚类中点的个数(随机产生):20
请输入初始化聚类中心个数(随机产生):4
初始聚类中心:
9.0 9.0
3.0 5.0
4.0 9.0
5.0 0.0
以<9.0,9.0>为中心的点:
8.0,9.0
9.0,8.0
7.0,8.0
9.0,9.0
新的聚类中心:8.25,8.5
以<3.0,5.0>为中心的点:
3.0,5.0
5.0,3.0
3.0,5.0
1.0,6.0
2.0,7.0
5.0,5.0
1.0,3.0
5.0,6.0
0.0,2.0
新的聚类中心:2.7777777,4.6666665
以<4.0,9.0>为中心的点:
6.0,9.0
4.0,9.0
新的聚类中心:5.0,9.0
以<5.0,0.0>为中心的点:
8.0,1.0
5.0,0.0
8.0,1.0
8.0,1.0
7.0,2.0
新的聚类中心:7.2,1.0
distcore:0.9013878188659973
以<8.25,8.5>为中心的点:
8.0,9.0
9.0,8.0
7.0,8.0
9.0,9.0
新的聚类中心:8.25,8.5
以<2.7777777,4.6666665>为中心的点:
3.0,5.0
5.0,3.0
3.0,5.0
1.0,6.0
2.0,7.0
5.0,5.0
1.0,3.0
5.0,6.0
0.0,2.0
新的聚类中心:2.7777777,4.6666665
以<5.0,9.0>为中心的点:
6.0,9.0
4.0,9.0
新的聚类中心:5.0,9.0
以<7.2,1.0>为中心的点:
8.0,1.0
5.0,0.0
8.0,1.0
8.0,1.0
7.0,2.0
新的聚类中心:7.2,1.0
distcore:0.0
distcore:0.0
distcore:0.0
distcore:0.0
迭代完毕!!!!!
  • 复杂度分析
    时间复杂度:O(tKmn),其中,t为迭代次数,K为簇的数目,m为记录数,n为维数
    空间复杂度:O((m+K)n),其中,K为簇的数目,m为记录数,n为维数

三、使用K-Means算法需要考虑的问题

1、K如何确定

k-menas算法首先选择K个初始质心,其中K是用户指定的参数,即所期望的簇的个数。这样做的前提是我们已经知道数据集中包含多少个簇,但很多情况下,我们并不知道数据的分布情况,实际上聚类就是我们发现数据分布的一种手段,这就陷入了鸡和蛋的矛盾。如何有效的确定K值,这里大致提供几种方法:

  • 与层次聚类结合
    产生较好的聚类结果的一个有趣策略是,首先采用层次凝聚算法决定结果粗的数目,并找到一个初始聚类,然后用迭代重定位来改进该聚类。

  • 稳定性方法
    稳定性方法对一个数据集进行2次重采样产生2个数据子集,再用相同的聚类算法对2个数据子集进行聚类,产生2个具有k个聚类的聚类结果,计算2个聚类结果的相似度的分布情况。2个聚类结果具有高的相似度说明k个聚类反映了稳定的聚类结构,其相似度可以用来估计聚类个数。采用次方法试探多个k,找到合适的k值。

  • 系统演化方法
    系统演化方法将一个数据集视为伪热力学系统,当数据集被划分为K个聚类时称系统处于状态K。系统由初始状态K=1出发,经过分裂过程和合并过程,系统将演化到它的稳定平衡状态Ki,其所对应的聚类结构决定了最优类数Ki。系统演化方法能提供关于所有聚类之间的相对边界距离或可分程度,它适用于明显分离的聚类结构和轻微重叠的聚类结构。

  • 使用canopy算法进行初始划分
    基于Canopy Method的聚类算法将聚类过程分为两个阶段:
    Stage1、聚类最耗费计算的地方是计算对象相似性的时候,Canopy Method在第一阶段选择简单、计算代价较低的方法计算对象相似性,将相似的对象放在一个子集中,这个子集被叫做Canopy ,通过一系列计算得到若干Canopy,Canopy之间可以是重叠的,但不会存在某个对象不属于任何Canopy的情况,可以把这一阶段看做数据预处理;
    Stage2、在各个Canopy 内使用传统的聚类方法(如K-means),不属于同一Canopy 的对象之间不进行相似性计算。
    从这个方法起码可以看出两点好处:首先,Canopy 不要太大且Canopy 之间重叠的不要太多的话会大大减少后续需要计算相似性的对象的个数;其次,类似于K-means这样的聚类方法是需要人为指出K的值的,通过Stage1得到的Canopy 个数完全可以作为这个K值,一定程度上减少了选择K的盲目性。

  • 其他方法如贝叶斯信息准则方法(BIC)等。

2、初始质心的选取

选择适当的初始质心是基本kmeans算法的关键步骤。常见的方法是随机的选取初始质心,但是这样簇的质量常常很差。
处理选取初始质心问题的一种常用技术是:多次运行,每次使用一组不同的随机初始质心,然后选取具有最小SSE(误差的平方和)的簇集。这种策略简单,但是效果可能不好,这取决于数据集和寻找的簇的个数。

第二种有效的方法是,取一个样本,并使用层次聚类技术对它聚类。从层次聚类中提取K个簇,并用这些簇的质心作为初始质心。该方法通常很有效,但仅对下列情况有效:(1)样本相对较小,例如数百到数千(层次聚类开销较大);(2)K相对于样本大小较小

第三种选择初始质心的方法,随机地选择第一个点,或取所有点的质心作为第一个点。然后,对于每个后继初始质心,选择离已经选取过的初始质心最远的点。使用这种方法,确保了选择的初始质心不仅是随机的,而且是散开的。但是,这种方法可能选中离群点。此外,求离当前初始质心集最远的点开销也非常大。为了克服这个问题,通常该方法用于点样本。由于离群点很少(多了就不是离群点了),它们多半不会在随机样本中出现。计算量也大幅减少。

第四种方法就是上面提到的canopy算法。

3、距离的度量

常用的距离度量方法包括:欧几里得距离和余弦相似度。两者都是评定个体间差异的大小的。欧几里得距离度量会受指标不同单位刻度的影响,所以一般需要先进行标准化,同时距离越大,个体间差异越大;空间向量余弦夹角的相似度度量不会受指标刻度的影响,余弦值落于区间[-1,1],值越大,差异越小。

从几何意义上来说,n维向量空间的一条线段作为底边和原点组成的三角形,其顶角大小是不确定的。也就是说对于两条空间向量,即使两点距离一定,他们的夹角余弦值也可以随意变化。感性的认识,当两用户评分趋势一致时,但是评分值差距很大,余弦相似度倾向给出更优解。举个极端的例子,两用户只对两件商品评分,向量分别为(3,3)和(5,5),这两位用户的认知其实是一样的,但是欧式距离给出的解显然没有余弦值合理。

4、质心的计算

对于距离度量不管是采用欧式距离还是采用余弦相似度,簇的质心都是其均值,即向量各维取平均即可。对于距离度量采用其它方式时,这个还没研究过。

5、算法停止条件

一般是目标函数达到最优或者达到最大的迭代次数即可终止。对于不同的距离度量,目标函数往往不同。当采用欧式距离时,目标函数一般为最小化对象到其簇质心的距离的平方和,如下:

K-means算法的实现原理和分析

当采用余弦相似度时,目标函数一般为最大化对象到其簇质心的余弦相似度和,如下:

K-means算法的实现原理和分析

6、空聚类的处理

如果所有的点在指派步骤都未分配到某个簇,就会得到空簇。如果这种情况发生,则需要某种策略来选择一个替补质心,否则的话,平方误差将会偏大。一种方法是选择一个距离当前任何质心最远的点。这将消除当前对总平方误差影响最大的点。另一种方法是从具有最大SSE的簇中选择一个替补的质心。这将分裂簇并降低聚类的总SSE。如果有多个空簇,则该过程重复多次。另外,编程实现时,要注意空簇可能导致的程序bug。

7、kmeans最佳实践

  • 随机选取训练数据中的k个点作为起始点
  • 当k值选定后,随机计算n次,取得到最小开销函数值的k作为最终聚类结果,避免随机引起的局部最优解
  • 手肘法选取k值:绘制出k--开销函数闪点图,看到有明显拐点(如下)的地方,设为k值,可以结合轮廓系数。
  • k值有时候需要根据应用场景选取,而不能完全的依据评估参数选取。

四、Kmeans算法的缺陷和改进

1、缺陷

Kmenas算法试图找到使平凡误差准则函数最小的簇。当潜在的簇形状是凸面的,簇与簇之间区别较明显,且簇大小相近时,其聚类结果较理想。前面提到,该算法时间复杂度为O(tKmn),与样本数量线性相关,所以,对于处理大数据集合,该算法非常高效,且伸缩性较好。但该算法除了要事先确定簇数K和对初始聚类中心敏感外,经常以局部最优结束,同时对“噪声”和孤立点敏感,并且该方法不适于发现非凸面形状的簇或大小差别很大的簇。

Kmeans算法的聚类中心的个数K 需要事先给定,但在实际中这个 K 值的选定是非常难以估计的,很多时候,事先并不知道给定的数据集应该分成多少个类别才最合适。Kmeans需要人为地确定初始聚类中心,不同的初始聚类中心可能导致完全不同的聚类结果。针对上述第2个缺陷,可以使用Kmeans++算法来解决。

2、K-Means ++ 算法

k-means++算法选择初始聚类中心的基本思想就是:初始的聚类中心之间的相互距离要尽可能的远。具体方法如下:

  • 从输入的数据点集合中随机选择一个点作为第一个聚类中心;
  • 对于数据集中的每一个点x,计算它与最近聚类中心(指已选择的聚类中心)的距离D(x);
  • 选择一个新的数据点作为新的聚类中心,选择的原则是:D(x)较大的点,被选取作为聚类中心的概率较大;
  • 重复2和3直到k个聚类中心被选出来;
  • 利用这k个初始的聚类中心来运行标准的k-means算法。

从上面的算法描述上可以看到,算法的关键是第3步,如何将D(x)反映到点被选择的概率上,一种算法如下:

  • 先从我们的数据库随机挑个随机点当“种子点”;
  • 对于每个点,我们都计算其和最近的一个“种子点”的距离D(x)并保存在一个数组里,然后把这些距离加起来得到Sum(D(x));
  • 然后,再取一个随机值,用权重的方式来取计算下一个“种子点”。这个算法的实现是,先取一个能落在Sum(D(x))中的随机值Random,然后用Random -= D(x),直到其<=0,此时的点就是下一个“种子点”。
    重复2和3直到k个聚类中心被选出来;
  • 利用这k个初始的聚类中心来运行标准的k-means算法。
    可以看到算法的第三步选取新中心的方法,这样就能保证距离D(x)较大的点,会被选出来作为聚类中心了。至于为什么原因比较简单,如下图 所示:

K-means算法的实现原理和分析
假设A、B、C、D的D(x)如上图所示,当算法取值Sum(D(x))*random时,该值会以较大的概率落入D(x)较大的区间内,所以对应的点会以较大的概率被选中作为新的聚类中心。