K_means聚类算法
这一期给大家带来的是K_means算法的基础教学及代码实现,如果讲的透彻别忘了收藏,当然,如果遇到任何问题也可以在评论区留言,我将及时回复。
K_means聚类算法简单来说就是将空间中的数据按照某些特征进行分类,如何分类呢?难点就在于数据量太大,海量空间数据的大小和复杂性都在快速增长,已经远远超出了人们的理解能力,所以从这些空间数据中发现邻域知识迫切需要一些空间聚类分析算法,这些算法将是从海量数据中发现知识的一个重要手段,而在这些空间聚类分析算法中,K-Means算法作为一种无监督的聚类算法,它实现起来比较简单,聚类效果也不错,因此在空间数据分析中应用很广泛,同时也起着重要的作用。
目录
一、问题描述
首先从数据集中随机选取K个点作为初始聚类中心,然后计算各个样本到聚类中心的距离,把样本点归到离它最近的那个聚类中心所在的类。计算新形成的每一个聚类数据对象的平均值来得到新的聚类中心,如果相邻两次的聚类中心没有任何变化,说明样本调整结束,所有数据已经完成分类。
因为在迭代过程中数据集的K个中心点在不停的变化,所以在每次迭代过程中都需要对所有的样本点进行重新分类,当中心点不再移动时,此时的分类结果就是最终的结果,这种在迭代过程中不断改变中心点和重新分类的过程,是K-means算法最大的特点。
二、问题分析
算法:K_means
步骤:
(1)对于要分类的数据集,任意从其中选择K个点作为初始的聚类中心点;
(2)计算数据集中每个点到中心点的距离,根据距离将其划分到对应的聚类中;
(3)计算聚类中所有点的中心点,作为新的聚类中心点;
(4)判断新的中心点与旧的中心点之间的距离是否符合要求,不符合则返回步骤(2),直到两个中心点距离相差符合要求。
三、代码实现
1. 数据集从哪儿来?
为了方便理解,我们举个例子:
小欣是一个女孩子,有一组三围数据:88,68,94,那么我们可以将这组数据用一个三维的点来表示(88,68,94),如果有很多女孩子,那么我们就可以用(86,66,90)(84,64,86)……等多个点来表示,这么多个点放在一起,就构成了一个数据集,而每个点上的维度(三围可以看成是x,y,z三个坐标),就表示了现实中的采集到的几个特征点。
这种以点的维度来表示现实物体特征的方法很常用,我们本次算法便利用这种原理。
假设我们的数据集中有50个点,且每个点都是3维的,那么就可以构建一个二维数组dataSet[50][3],生成这个数组原始数据的过程,就是获得数据集的过程。
当然我们不可能真的就找50个三围一个个输入,那太可怕了,为了快速获取原始数据,我们将直接使用随机函数(产生0到100之间的数)对这个二维数组进行填充。
for (int i = 0; i < volume; ++i) { // Randomly generate 50 3D coordinate points
for (int j = 0; j < dimension; ++j) {
dataSet[i][j] = rand()%100; //the random figure between 0 - 10
}
}
(代码中volume为数据集的容量,即50,dimension为点的维度,即3)
2. 初始中心点怎么设置?
初始中心点怎么设置?这个问题可以拆分成两个小问题:
–>第一个问题是:我们设置几个中心点?
为了方便展示,我们仅设置3个中心点,预计将100个点分为三个类;
–>第二个问题是:这个中心点怎么赋初值?
因为中心点要来回移动,我们需要有3个新中心点和3个旧中心点的位置,即我们可以在dataSet数组后边再扩展6个位置,即50,51,52三个点代表旧的中心点,53,54,55三个点代表新的中心点,新中心点可以直接赋初值为(0,0,0)(0,0,0)(0,0,0),旧中心点可以从前边50个点中随机找三个点把其值复制过来。
for (int i = 0; i < 3; ++i) {
int randNum = rand()%volume;
for (int j = 0; j < dimension; ++j) // Randomly select 3 points which have exited as the center point(50,51,52)
dataSet[volume + i][j] = dataSet[randNum][j];
for (int j = 0; j < dimension; ++j) // the 3 new center points set are 0(53,54,55)
dataSet[volume + i + 3][j] = 0;
}
3. 如何计算数据集中任意两个点的距离?
前边的两个问题解决了数据集的由来以及中心点的确立,因为数据集上的点都要分别和每个中心点比较,那么怎么计算点与点之间的距离呢?
是的,或许你想到了,计算两个点之间的距离,这是初中知识:
double dist(int point_1, int point_2, double dataSet[][dimension]){ //Calculate the distance between two points
double distx = dataSet[point_1][0] - dataSet[point_2][0];
double disty = dataSet[point_1][1] - dataSet[point_2][1];
double distz = dataSet[point_1][2] - dataSet[point_2][2];
double dist = sqrt(distx*distx + disty*disty + distz*distz);
return dist;
}
4. 怎么判断某个数据点从属于哪个类?
归类问题我们可以转化为比较距离的问题,因为一个点到每个中心点的距离是不一样的,我们按照离哪个中心点距离最短的原则,把这个点化为那一类;
这里给出一个比较最短距离的函数,并返回编号(50-A类-0,51-B类-1,52-C类-2):
int minDist(double dist_1, double dist_2, double dist_3){ //Calculate which distance is the shortest
if(dist_1 <= dist_2 && dist_1 <= dist_3)
return 0; //belong to class A
if(dist_2 <= dist_1 && dist_2 <= dist_3)
return 1; //belong to class B
if(dist_3 <= dist_1 && dist_3 <= dist_2)
return 2; //belong to class
}
能计算两点之间的距离,能判断距离哪个中心点最近,接下来我们需要3个数组来存储划分到三个类中的点:
int class_A[volume], class_B[volume], class_C[volume];
注意,这三个数组最大容量我设置的都是50,那么肯定存不满,如果存不满的话后期怎么取出里边的内容呢?因此对这三个数组初始化很重要(都初始化为-1,从数组取数字的时候,可以根据-1设置终止条件):
for (int i = 0; i < volume; ++i) {
class_A[i] = -1;
class_B[i] = -1;
class_C[i] = -1;
}
数据集中的一个点分别和三个中心点计算距离,产生的结果用minDist函数确定距离哪个中心点最近,并存入对应的类中,这是对一个点需要完成的操作,操作完成后,进行下一个点的操作,直到所有点都完成了一轮的分类。
5. 怎样计算新的中心点?
每一轮所有点都完成分类后,三个类(class_A,class_B,class_C三个数组)中已经存放了分类好的点,那么如何计算新的中心点?
拿class_A类来说,最直接的办法就是把A类中所有点的x坐标相加求平均值,得到新的x坐标,所有点的y坐标相加求平均值,得到新的y坐标,所有点的z坐标相加求平均值,得到新的z坐标;
同样的方法,也可以求出class_B和class_C两个类的新中心点:
void newCenter(const int class[volume], double dataSet[][dimension], int num){ //calculate the new center point
double x = 0, y = 0, z = 0; //num is mean which new center point
for (int i = 0; class[i] != -1; ++i) { // class must be initialized with -1
x += dataSet[class[i]][0];
y += dataSet[class[i]][1];
z += dataSet[class[i]][2];
}
double Average_x = x/volume;
double Average_y = y/volume;
double Average_z = z/volume;
dataSet[volume + 3 + num][0] = Average_x;
dataSet[volume + 3 + num][1] = Average_y;
dataSet[volume + 3 + num][2] = Average_z;
}
当然,计算出了新的中心点,就需要把它存储进dataSet数组中,即53,54,55位置。
6.如何判定是否已经分类完成?
对于A类,计算A类新中心点与旧中心点的距离(即50与53求距离),若距离小于0.0005,则A类已经聚合;
对于B类,计算B类新中心点与旧中心点的距离(即51与54求距离),若距离小于0.0005,则B类已经聚合;
对于C类,计算C类新中心点与旧中心点的距离(即52与55求距离),若距离小于0.0005,则C类已经聚合;
当上边三个类都聚合后,数据集的分类已经完成,否则就开启下一轮迭代。
7. 新旧中心点怎样实现更换?
计算出了新的中心点后,若还需进行下一轮迭代,则需要把这几个中心点赋值到旧中心点的位置上(一方面是计算需要,另一方面也是53,54,55位置要空出来留给下一次产生中心点的需要),通俗的说,就是50 = 53, 51 = 54, 52 = 55
void refreshCenter(double dataSet[][3]) { //refresh the old center points
for (int i = 0; i < 3; ++i)
for (int j = 0; j < dimension; ++j)
dataSet[volume + i][j] = dataSet[volume + i + 3][j];
}
8. 算法的运行结果如何查看?
大家可能最关注的就是查看结果了,是的,有些算法只是一个逻辑模型,必须运用于场景中才能观察到效果,但这里我用C程序设计实现的K_means算法可以查看到结果:
void show(const int class[], double dataSet[][dimension], int num){ //output the result of the K_means
char className = '\0';
if(num == 0) className = 'A';
else if(num == 1) className = 'B';
else if(num == 2) className = 'C';
for (int i = 0; class[i] != -1; ++i)
printf("(%f,%f,%f)->%c\n", dataSet[class[i]][0], dataSet[class[i]][1], dataSet[class[i]][2], className);
}
好了,讲解部分已经完毕,你可以尝试用自己的方法将上边的代码串起来,当然也可以借鉴一下我下边的源码,如有问题欢迎留言。
四、源码
#include <stdio.h>
#include "time.h"
#include "stdlib.h"
#include "math.h"
#define volume 50 //the data set's volume
#define dimension 3 //the dimension
#define condition 0.0005 //Conditions for data convergence
void init(double dataSet[][dimension]){ // Initialize the data set
srand((unsigned)time(NULL));
for (int i = 0; i < volume; ++i) { // Randomly generate 50 3D coordinate points
for (int j = 0; j < dimension; ++j) {
dataSet[i][j] = rand()%100; //the random figure between 0 - 10
}
}
for (int i = 0; i < 3; ++i) {
int randNum = rand()%volume;
for (int j = 0; j < dimension; ++j) // Randomly select 3 points which have exited as the center point(50,51,52)
dataSet[volume + i][j] = dataSet[randNum][j];
for (int j = 0; j < dimension; ++j) // the 3 new center points set are 0(53,54,55)
dataSet[volume + i + 3][j] = 0;
}
}
double dist(int point_1, int point_2, double dataSet[][dimension]){ //Calculate the distance between two points
double distx = dataSet[point_1][0] - dataSet[point_2][0];
double disty = dataSet[point_1][1] - dataSet[point_2][1];
double distz = dataSet[point_1][2] - dataSet[point_2][2];
double dist = sqrt(distx*distx + disty*disty + distz*distz);
return dist;
}
int minDist(double dist_1, double dist_2, double dist_3){ //Calculate which distance is the shortest
if(dist_1 <= dist_2 && dist_1 <= dist_3)
return 0; //belong to class A
if(dist_2 <= dist_1 && dist_2 <= dist_3)
return 1; //belong to class B
if(dist_3 <= dist_1 && dist_3 <= dist_2)
return 2; //belong to class
}
int judge(double dataSet[][dimension]){ //Judge whether the new and old center points have changed
double dist_center1 = dist(volume, volume + 3, dataSet);
double dist_center2 = dist(volume + 1, volume + 4, dataSet);
double dist_center3 = dist(volume + 2, volume + 5, dataSet);
if(dist_center1 > condition || dist_center2 > condition || dist_center3 > condition)
return 1; //Any one greater than 0.005 will be judged as non-convergent
else
return 0; //The data set has converged
}
void newCenter(const int class[volume], double dataSet[][dimension], int num){ //calculate the new center point
double x = 0, y = 0, z = 0; //num is mean which new center point
for (int i = 0; class[i] != -1; ++i) { // class must be initialized with -1
x += dataSet[class[i]][0];
y += dataSet[class[i]][1];
z += dataSet[class[i]][2];
}
double Average_x = x/volume;
double Average_y = y/volume;
double Average_z = z/volume;
dataSet[volume + 3 + num][0] = Average_x;
dataSet[volume + 3 + num][1] = Average_y;
dataSet[volume + 3 + num][2] = Average_z;
}
void refreshCenter(double dataSet[][3]) { //refresh the old center points
for (int i = 0; i < 3; ++i)
for (int j = 0; j < dimension; ++j)
dataSet[volume + i][j] = dataSet[volume + i + 3][j];
}
void show(const int class[], double dataSet[][dimension], int num){ //output the result of the K_means
char className = '\0';
if(num == 0) className = 'A';
else if(num == 1) className = 'B';
else if(num == 2) className = 'C';
for (int i = 0; class[i] != -1; ++i)
printf("(%f,%f,%f)->%c\n", dataSet[class[i]][0], dataSet[class[i]][1], dataSet[class[i]][2], className);
}
void K_meansAlgo(){
double dataSet[volume + 6][dimension];
int class_A[volume], class_B[volume], class_C[volume];
int lenClass_A = 0, lenClass_B = 0, lenClass_C = 0;
//initialize the data set, center points and class
init(dataSet);
//refresh the center points
double dist_0, dist_1, dist_2;
long int p = 1; //Cycle count
while(1){
for (int i = 0; i < volume; ++i) {
class_A[i] = -1;
class_B[i] = -1;
class_C[i] = -1;
}
lenClass_A = lenClass_B = lenClass_C = 0;
//calculate the distance between the point to each center point, then due to the minimum
//distance to classify it to witch class
for (int i = 0; i < volume; ++i) {
dist_0 = dist(i, volume, dataSet);
dist_1 = dist(i, volume + 1, dataSet);
dist_2 = dist(i, volume + 2, dataSet);
if(minDist(dist_0, dist_1, dist_2) == 0) {
class_A[lenClass_A] = i;
lenClass_A ++;
}else if(minDist(dist_0, dist_1, dist_2) == 1){
class_B[lenClass_B] = i;
lenClass_B ++;
}else if(minDist(dist_0, dist_1, dist_2) == 2){
class_C[lenClass_C] = i;
lenClass_C ++;
}
}
//calculate the new center points
newCenter(class_A, dataSet, 0); //center point 0
newCenter(class_B, dataSet, 1); //center point 1
newCenter(class_C, dataSet, 2); //center point 2
//judge the new center points and the old points weather to meet the conditions
printf("第%ld轮收敛\n", p++);
//Determine whether the new and old center points meet the convergence conditions
if(judge(dataSet) == 0) break;
else refreshCenter(dataSet); //refresh the old center
}
//show the result
show(class_A, dataSet, 0);
printf("\n");
show(class_B, dataSet, 1);
printf("\n");
show(class_C, dataSet, 2);
printf("\n");
}
int main() {
K_meansAlgo();
return 0;
}
五、代码运行结果
程序一共迭代了三次,中心点的移动幅度便满足了我设定的0.0005标准。
六、常见问题及解决
- 这年代电脑运行都很快,如果你的数据集也就一百来个数,电脑迭代了100轮都还没结果的时候,注意检查是不是哪里写错了;
- 如果一轮迭代后聚类效果不满足要求,这个时候需要将新产生的中心点赋值给旧的中心点,如果没有这一步操作的话迭代将陷入无限循环;
- 函数的编写要能被更多的使用者使用,如计算两点距离的函数,那么我们计算中心点到中心点的距离、点到中心点的距离、点到点的距离时都可以引用;
- 数据集的大小和数据的维度我一直用的是宏定义,也建议你这么处理,因为可以在后期随时修改数据集大小和维度,提高了程序的灵活性。