聚类算法的实现 k-means(一)
说来这个聚类算法的实现是数据挖掘课程的第三次作业了,前两次的作业都是利用别人的软件,很少去自己实现一个算法,第一个利用sqlserver2008的商业智能工具实现一个数据仓库,数据处理,仓库模型的建立绕,维度表,事实表的创建,不过考试的时候应该也会有数据仓库常用模型的建立吧;第二次利用weka的分类和关联规则算法跑一些提供的数据,其实那些算法的参数原理都不晓得;
不过这次的聚类作业竟然是实现一个自己的算法针对提供的数据进行聚类,先描述一下提供的要聚类的数据,主要是两个数据集:
针对数据集1能够很清楚的看出聚类的意图,但是数据集2不太明白数据聚类的意图;针对数据集1可以看出利用欧几里得距离可以利用k-means算法进行聚类,但是当第一个写完了之后发现效果也不是很好,并且k-means好像不能够对第二个数据集进行聚类,感觉到要重写的节奏,但是这里还是描述一下第一个数据集进行写程序的过程;
数据的设定,当初设计程序的时候考虑到了兼容性,数据集1和2,就设计了一个父类,然后数据集1和2分别集成父类,这样就可以利用多态性了,数据里可以直接存放父类的指针,这样就不用考虑存放的具体的是数据集1或者2,但是现在考虑到第二个数据集不能利用k-means;算法需要修改,但是这个结构可以保留;
继承的层次如下:
父类为一个抽象类,定义了几个子类继承重写的几个抽象方法,如下:
其中方法的作用主要是计算距离;计算准则收敛函数;重新计算均值;画图函数;属性值有颜色值和聚类类别标示;
两个子类实现以上方法并且存在自己的属性,主要如下:
这次程序设计过程中程序不是基于Dialog的,而是选择的MFC提供的单文档结构,包含View、Doc、Frame类,其中读文件部分重写Doc类中的OnOpenDocument函数即可;
BOOL CDataMiningExample1Doc::OnOpenDocument(LPCTSTR lpszPathName) { if (!CDocument::OnOpenDocument(lpszPathName)) return FALSE; // 打开文件然后将数据读入数据结构中 CMainFrame* pMainFrame=(CMainFrame*)(AfxGetApp()->m_pMainWnd); CDataMiningExample1View* pMainView=(CDataMiningExample1View*)(pMainFrame->GetActiveView()); //读入之前清空 pMainView->emptyCluster(); pMainView->emptyKmeans(); //读入数据至vector //2D //打开输入流 ifstream infile(lpszPathName); double nCX,nCY; int nCR,nCG,nCB; //判断是哪个文件 string str; getline(infile,str); int length = str.size(); infile.seekg(0); while (!infile.eof()) { if (length>11) { infile>>nCX; infile>>nCY; infile>>nCR; infile>>nCG; infile>>nCB; if (nCR==255&&nCG==255&&nCB==255) { continue; } else { DataPTColor* nDataPTColor = new DataPTColor(nCX,nCY,nCR,nCG,nCB); pMainView->m_cCluster.m_dataPoints.push_back(nDataPTColor); } } else { infile>>nCX; infile>>nCY; //读入数据构造要求的点 //这里应该判断文件结构选择构造哪个对象,这里仅第一题数据, DataPT2D* nDataPT2D = new DataPT2D(nCX,nCY); pMainView->m_cCluster.m_dataPoints.push_back(nDataPT2D); } } return TRUE; }
接着是想要在聚类绘制点的过程中刷新屏幕动态的显示点的颜色的变化,一开始选择在界面的线程中运行算法,然后开线程invalidate更新界面,但是后来发现这样每次运行算法的时候界面就卡着了,后来发现实现这种方式,应该是界面不停的刷新,然后开辟新的线程不断的更新界面要显示的内容,即后台的数据,所以这里应该开辟一个线程运行算法,然后界面仅仅负责根据数据绘制即可;
然而invalidate绘制会出现一闪一闪的情况,所以这里要利用双缓冲,但是这个技术还不知道怎么用,所以这里先保留一下;
其实k-means的算法比较简单,也正是比较简单,当时选择了这个算法,但是好像第二个数据集的特征不适合这个算法,这个算法适合圆形簇和球形簇的结构的发现;k-means的算法如下:
void Cluster::kmeans(vector<AbstractDataPT*> initKmeans) { //k-means迭代算法 //每个点有一个类别 vector<AbstractDataPT*> tmpKmeans(initKmeans); int i,j,k=tmpKmeans.size(),length=m_dataPoints.size(); //保留最短距离 double shortest; //准则函数的值 double newFunction; int num=0; //具体迭代过程 while (TRUE) { num++; for (i=0;i<length;i++) { shortest=MAX; for (j=0;j<k;j++) { //每个点与k个均值计算距离 double tpdistance=m_dataPoints[i]->calculateD(tmpKmeans[j]); //距离某个点近则更新为某个点的颜色,并且把点的类别至于j+1类别 if (tpdistance<shortest) { shortest=tpdistance; //更新颜色 m_dataPoints[i]->m_colorPT=tmpKmeans[j]->m_colorPT; //更新类别 m_dataPoints[i]->categroy=j+1; } } } //计算准则函数 newFunction=m_dataPoints[0]->calculateE(m_dataPoints,tmpKmeans); if (fabs(lastFunction-newFunction)>0.00001) { lastFunction=newFunction; } else { break; } //重新计算均值k tmpKmeans.clear(); tmpKmeans=m_dataPoints[0]->calculateMeans(m_dataPoints,k); Sleep(2000); initKmeans=tmpKmeans; //检测收敛后跳出循环 } }
还有一些体会就是,面向对象的思想运用到程序中就是设计一个点的类,绘制应该也是这个类自己事情,所以对象应该有绘制自己的方法~其他的一些就是程序中在任何地方可以利用一下一段代码获得View类:
CMainFrame* pMainFrame=(CMainFrame*)(AfxGetApp()->m_pMainWnd); CDataMiningExample1View* pMainView=(CDataMiningExample1View*)(pMainFrame->GetActiveView());
View类这里就是负责显示的一个类,其实Doc类和View类这里都可以互相获得对方的指针的,所以这些知识还要是积累一下;
下面截取一张聚类的结果图,效果不怎么好:
第二个根据点的坐标数据集的k-means算法的聚类如下,结果不如人意,所以k-means算法对于这个数据集的聚类分析不是很有效,接下来如果实现了更有效的方法会进行更新比较:
修正:第二题目预处理的时候这里忽略了一些数据的问题,所以第二个数据集聚类出效果不是很好,这里修正一下,不只是取出白色的点,这里过滤条件增强,去除稍微浅色的点,这里暂时设置如下:
if (nCR>235&&nCG>235&&nCB>235) { continue; }
这样处理还有一个好处就是过滤了一部分点,点的数量降低了,算法的运算速度增加了,能够比较快的得出结果了,后面的DBSCAN时间还是相对来说比较慢,不知道索引如何建立,所以邻域的计算还是要进行所有点的遍历;
这样就可以得到一个比较清晰的图像了,截图如下,这里图形进行了放大,所以显示出来有部分没有显示: