K均值聚类算法在cxcoer中,因为它在ML库诞生之前就存在了.K均值尝试找到数据的自然类别.用户设置类别个数,K均值迅速地找到"好的"类别中心."好的"意味着聚类中心位于数据的自然类别中心.K均值是最常用的聚类计数之一,与高斯混合中的期望最大化算法(在ML库中实现为CvEM)很相似,也与均值漂移算法(在CV库中实现为cvMeanShift())相似.K均值是一个迭代算法,在OpenCV中采用的是Lloyd算法,也叫Voronoi迭代.算法运行如下.
1.输入数据集合和类别数K(由用户指定)
2.随机分配类别中心点的位置
3.将每个点放入离它最近的类别中心点所在的集合.
4.移动类别中心点到它所在集合的中心
5.转到第3步,直到收敛.
图13-5展示了K均值是怎么工作的.在这个例子中,只用两次迭代就达到了收敛.在现实数据中,算法经常很快的收敛,但是有的时候需要迭代的次数比较多.
问题和解决方案
K均值是一个及其高效的聚类算法,但是它也有以下3个问题.
1.它不保证能找到定位聚类中心的最佳方案,但是它能保证能收敛到某个解决方案(例如迭代不再无限继续下去).
2.K均值无法指出应该使用多少类别.在同意数据集中,例如对图13-5中的例子,选择两个类别和选择四个类别,得到的结果是不一样的,甚至是不合理的.
3.K均值假设空间的协方差矩阵不会影响结果,或者已经归一化(参考Mahalanobis距离的讨论).
这三个问题都有"解决办法".这些解决方法中最好的两种都是基于"劫色数据的方差".在K均值中,每个聚类中线拥有他的数据点,我们计算这些点的方差,最好的聚类在不引起太大的复杂度的情况下使方差达到最小.所以,列出的问题可以改进如下.
1.多进行几次K均值,每次初始的聚类中心点都不一样(很容易做到,因为OpenCV随机选择中心点),最后选择方差最小的那个结果
2.首先将类别数设为1,然后提高类别数(到达某个上限),每次聚类的时候使用前面提到的方法1.一般情况下,总方差会很快下降,直到到达一个拐点;这意味着再加一个新的聚类中心不会显著地减少总方差.在拐点处停止,保存此时的类别数.
3.将数据乘以逆协方差矩阵.例如:如果输入向量是按行排列,没行一个数据点,则通过计算新的数据向量D*,D* = D∑-1/2来归一化数据空间.
K均值代码
K均值函数的调用很简单:
void cvKMeans2(const CvArr* samples, int cluster_count,CvArr* labels,CvTrtmCriteria termcrit);
数组sample是一个多维的数据样本矩阵,每行一个数据样本.这里有一点点微妙,数据样本可以是浮点型CV_32FC1向量,也可以是CV_32FC2,CV_32FC3和CV_32FC(k)的多维向量.参数cluster_count指定类别数,返回向量包含每个点最后的类别索引.前面已经讨论了terncrit.
- #include <QCoreApplication>
- #include <opencv2/highgui/highgui.hpp>
- #include <opencv2/core/core.hpp>
-
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
-
- #define MAX_CLUSTER 5
- CvScalar color_table[MAX_CLUSTER];
- IplImage* img = cvCreateImage(cvSize(500,500),8,3);
- CvRNG rng = cvRNG(0xffffffff);
-
- color_table[0] = CV_RGB(255,0,0);
- color_table[1] = CV_RGB(0,255,0);
- color_table[2] = CV_RGB(100,100,255);
- color_table[3] = CV_RGB(255,0,255);
- color_table[4] = CV_RGB(255,255,0);
-
- cvNamedWindow("clusters",1);
- for(;;)
- {
- int k,cluster_count = cvRandInt(&rng)%MAX_CLUSTER +1;
- int i,sample_count = cvRandInt(&rng)%1000+1;
- CvMat* points = cvCreateMat(sample_count,1,CV_32FC2);
- CvMat* clusters = cvCreateMat(sample_count,1,CV_32SC1);
-
-
-
-
-
- for(k=0;k < cluster_count;k++)
- {
- CvPoint center;
- CvMat point_chunk;
- center.x = cvRandInt(&rng)%img->width;
- center.y = cvRandInt(&rng)%img->height;
-
-
-
-
- cvGetRows(points,&point_chunk,k*sample_count/cluster_count,
- k == cluster_count -1? sample_count:
- (k+1)*sample_count/cluster_count);
-
-
-
- cvRandArr(&rng,&point_chunk,CV_RAND_NORMAL,
- cvScalar(center.x,center.y,0,0),
- cvScalar(img->width/6,img->height/6,0,0));
- }
-
- for(i=0;i<sample_count/2;i++)
- {
-
- CvPoint2D32f* pt1=(CvPoint2D32f*)points->data.fl+cvRandInt(&rng)%sample_count;
- CvPoint2D32f* pt2=(CvPoint2D32f*)points->data.fl+cvRandInt(&rng)%sample_count;
- CvPoint2D32f temp;
- CV_SWAP(*pt1,*pt2,temp);
- }
-
- cvKMeans2(points,cluster_count,clusters,cvTermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER,10,1.0));
- cvZero(img);
- for(i=0;i<sample_count;i++)
- {
- CvPoint2D32f pt = ((CvPoint2D32f*)points->data.fl)[i];
- int cluster_idx = clusters->data.i[i];
- cvCircle(img,cvPointFrom32f(pt),2, color_table[cluster_idx],CV_FILLED);
- }
- cvReleaseMat(&points);
- cvReleaseMat(&clusters);
- cvShowImage("clusters",img);
- int key = cvWaitKey(0);
- if(key==27)
- break;
- }
-
- return a.exec();
- }
在这段代码中,包含highgui来使用窗口输出,包含cxcore是因为它包含了Kmean2().在main函数中,我们设置了放回的类别显示的颜色;设置类别个数上界MAX_CLUSTERS(这是5),类别的个数是随机产生的,存储在cluster_count中;设置数据样本的个数的上界(1000),数据样本的个数也是随机产生的,被存储在sample_count中.在最外层循环for{]中,我们分配了一个浮点数双通道矩阵poing来存储sample_count个数据样本,我们还分配了一个整型矩阵clusters来存储数据样本的聚类标签,从0~cluster_count-1.
然后我们进入数据生成for{}循环,这个循环可以用于其他算法中使用.我们给每个类别填写sample_count/cluster_count个数据样本,这些2维数据样本服从正态分布,正态分布的中心是随机选择的.
下一个for{}循环仅仅打乱了数据样本的顺序.然后我们使用cvKMean2(),直到聚类中心的最大移动小于1.
最后的for{}循环话出结果,在图中显示.然后释放数组,等待用户的输入进入下一次计算或者Esc键退出.