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; //類別個數 1~5
int i,sample_count = cvRandInt(&rng)%1000+1; //樣本個數 1~1000
CvMat* points = cvCreateMat(sample_count,1,CV_32FC2); //創建sample_count行1列的雙通道矩陣 用於存儲數據樣本
CvMat* clusters = cvCreateMat(sample_count,1,CV_32SC1);//創建sample_count行1列的矩陣 用來存儲數據標籤
//隨機生成樣本多元高斯分佈
//樣本總數爲sample_count 類別總數爲cluster_count
//樣本矩陣爲points,先按類別分成cluster_count份 每一份數據的個數爲sample_count/cluster_count
//然後按類別隨機矩陣填充樣本矩陣,第一類填充矩陣的0~sample_count/cluster_count行
//第二類填充樣本矩陣的sample_count/cluster_count~sample_count/cluster_count*2行,以此類推直到填充滿所有矩陣,每一類的樣本個數是一樣的
for(k=0;k < cluster_count;k++)
{
CvPoint center; //爲圖像中的隨機點
CvMat point_chunk;
center.x = cvRandInt(&rng)%img->width;
center.y = cvRandInt(&rng)%img->height;
//返回數組在一定跨度的行
//points爲輸入數組,point_chunk返回數組
//開始行爲k*sample_count/cluster_count
//結束行爲 當k== cluster-1時 爲 第samplecount行 否則爲(k+1)*sample_count/cluster_count
cvGetRows(points,&point_chunk,k*sample_count/cluster_count,
k == cluster_count -1? sample_count:
(k+1)*sample_count/cluster_count);
//point_chunk輸出數組,CV_RAND_NORMAL分佈類型爲正態分佈或者高斯分佈
//cvScalar(center.x,center.y,0,0)隨機數的平均值
// cvScalar(img->width/6,img->height/6,0,0)如果是正態分佈它是隨機數的標準差
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++)
{
//在points樣本矩陣中隨機取兩個樣本交換位置
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);
}
//points樣本矩陣,cliuster_count分類數,輸出向量clusters,最後一個參數指定精度
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鍵退出.