說來這個聚類算法的實現是數據挖掘課程的第三次作業了,前兩次的作業都是利用別人的軟件,很少去自己實現一個算法,第一個利用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時間還是相對來說比較慢,不知道索引如何建立,所以鄰域的計算還是要進行所有點的遍歷;
這樣就可以得到一個比較清晰的圖像了,截圖如下,這里圖形進行了放大,所以顯示出來有部分沒有顯示: