1. 程式人生 > >層次聚類以及k-means演算法

層次聚類以及k-means演算法

一、實驗內容

給定國際通用UCI資料庫中FISHERIRIS資料集,其meas集包含150個樣本資料,每個資料含有鶯尾屬植物的4個屬性,即萼片長度、萼片寬度、花瓣長度,單位為cm。上述資料分屬於species集的三種setosa、versicolor和virginica花朵類別。
要求在該資料集上執行:
1. 層次聚類演算法
2. k-means聚類演算法
得到的聚類結果與species集的Label結果比較,統計這兩類演算法聚類的正確率和執行時間。

附件:Fisheriris資料集:

① fisheriris_meas.xls ( 鶯尾花4個屬性值 )
② fisheriris_species.xls ( fisheriris_meas.xls中每條資料相應的類別Label值 )

二、實驗設計(原理分析及流程)
將兩個資料檔案讀取到程式中,使用一個點結構矩陣來存放檔案的資訊。之後針對層次聚類演算法,使用一個計數器TotClu代表剩餘簇數目,當TotClu為3時結束演算法,演算法使用一個查詢函式每次查詢最短距離的兩個簇,接著使用合併演算法進行合併。接著可以顯處理完成的點結構矩陣以及計算分類的準確率。
之後使用一個儲存k-means演算法的初始簇的矩陣儲存初始簇(這裡使用層次聚類演算法算出來的離簇中心最近的點作為初始簇)。然後用使用k-means演算法來將所有點歸類到最近的簇裡面,每次歸類完都呼叫RecalCen函式來重新計算簇中心。之後再顯示處理完成的點結構矩陣以及準確率。截圖中因為一個命令列介面顯示不完程式需要執行兩次來顯示結果。中間部分的截圖省略了。
其中點的結構如下:
typedef struct Node
{
int CluNum; // 簇編號
double LenOne, WidOne, LenTwo, WidTwo; // 四個屬性值
double ShDis = 100.0; // 離其他簇的最短距離
int ShNum = 1000; // 最短距離的簇編號
} Node;
兩個演算法中,層次聚類的執行時間要比k-means更長,這裡資料量比較少,當資料量大時更加明顯,但層次聚類的準確率比較高。
而k-means演算法的執行時間更短,顯然它的實現更加簡單,但同時受到初始簇選擇的影響,其準確率依賴於初始簇的選擇以及初始簇的代表性,這個程式中,其準確率比層次聚類的準確率低。

三、對於k-means演算法,初始的簇有多種選取方式,程式碼中直接根據真正的資料集計算三個簇中心,找到距離中心最近的點作為k-means的三個初始點。同時,也可以使用層次聚類計算所得的三個簇中心找初始點,要求層次聚類演算法實現是正確的。

四、程式碼:

// cluster.cpp 對國際通用UCI資料庫中FISHERIRIS資料集執行
// 1.層次聚類2.k-means聚類演算法,比較兩者的執行時間以及準確率
// 為了方便處理,直接將資料檔案轉化為csv格式再進行處理

#include <iostream>
#include <fstream>
#include <string>
#include <vector> #include <cstdlib> #include <cmath> #define ProNum 4 // 點結構屬性個數 #define ProLine 150 // 點的個數 #define CentNum 3 // k-means初始簇中心個數 #define CPOne 50 // 品種資料檔案的兩個簇分界點編號1(從0開始) #define CPTwo 100 // 品種資料檔案的兩個簇分界點編號2(從0開始) typedef struct Node { int CluNum; // 簇編號 double LenOne, WidOne, LenTwo, WidTwo; // 四個屬性值 double ShDis = 100.0; // 離其他簇的最短距離 int ShNum = 1000; // 最短距離的簇編號 } Node; // 分割函式,將從檔案讀取的一行string分割成多個列 // 引數: 源字串,分界符,存放單詞的容器 返回0表示成功執行 int Split(const std::string &str, const std::string &splitchar, std::vector <std::string> &vec) { std::string stmp = ""; std::string::size_type pos = 0, prev_pos = 0; vec.clear(); // 刪除存在的元素 while ((pos = str.find_first_of(splitchar, pos)) != std::string::npos) { stmp = str.substr(prev_pos, pos - prev_pos); vec.push_back(stmp); prev_pos = ++pos; } stmp = str.substr(prev_pos, pos - prev_pos); if (stmp.length() > 0) { vec.push_back(stmp); } return 0; } // 結構矩陣生成函式 矩陣行號從0開始 // 引數:結構矩陣,存放屬性的矩陣 返回0表示成功執行 int NodMatGen(Node *NodMat, double ProMat[][ProNum]) { for (int i = 0; i < ProLine; i++) { NodMat[i].CluNum = i,NodMat[i].LenOne = ProMat[i][0]; NodMat[i].WidOne = ProMat[i][1],NodMat[i].LenTwo = ProMat[i][2]; NodMat[i].WidTwo = ProMat[i][3]; } return 0; } // 計算兩個簇的歐式距離 // 引數:簇1, 簇2 double DisCal(Node *NodMat1, Node *NodMat2) { double DIS; DIS = (pow((NodMat1->LenOne - NodMat2->LenOne),2) + pow((NodMat1->WidOne - NodMat2->WidOne),2)); DIS += (pow((NodMat1->LenTwo - NodMat2->LenTwo),2) + pow((NodMat1->WidTwo - NodMat2->WidTwo),2)); return sqrt(DIS); } // 找到當前所有簇之間的最短距離的兩個簇,返回一個簇結構,作為合併後的簇 // 引數:點結構矩陣 返回值:最短距離的簇編號 Node * FindSh(Node *NodMat) { int ShCluNum = 1000; // 最短距離的可以合併的簇編號 double DIS = 1000, NodShDis = 100.0; // 存放最短距離的臨時變數 for(int i = 0; i < ProLine - 1; i++) { // 計算每個簇到其他簇的最短距離 for (int j = i + 1; j < ProLine; j++) { if (NodMat[i].CluNum == NodMat[j].CluNum) continue; // 若兩個點屬於同一個簇則跳過距離計算 DIS = DisCal((NodMat+i), (NodMat+j)); if (NodMat[i].ShDis > DIS) // 新的距離小於原最短距離 { NodMat[i].ShDis = DIS; // 更新最短距離 NodMat[i].ShNum = NodMat[j].CluNum; // 更新最短的相鄰簇編號 } } } for (int i = 0; i < ProLine - 1; i++) { if (NodMat[i].ShDis < NodShDis) { NodShDis = NodMat[i].ShDis; ShCluNum = NodMat[i].CluNum; // 找到最短距離的簇編號 } } return (NodMat+ShCluNum); } // 將點合併到簇中,更新屬性值(此處將原結構陣列屬於相同簇的點都設定為簇心的屬性值) // 引數:合併後的點,點結構陣列 int NodMer(Node *DesNod, Node *NodMat) { // 存放重新計算的簇中心的四個屬性 double NewLenOne, NewWidOne, NewLenTwo, NewWidTwo; int DesNodNum = DesNod->CluNum; // 結果目標點的簇編號 int ForNodNum = DesNod->ShNum; // 將要併入的節點編號 NewLenOne = (DesNod->LenOne + NodMat[ForNodNum].LenOne) / 2; NewWidOne = (DesNod->WidOne + NodMat[ForNodNum].WidOne) / 2; NewLenTwo = (DesNod->LenTwo + NodMat[ForNodNum].LenTwo) / 2; NewWidTwo = (DesNod->WidTwo + NodMat[ForNodNum].WidTwo) / 2; for (int i = 0; i < ProLine; i++) // 對相同簇中的點進行屬性更新 { // 同時更新距離和最短距離簇編號 int ModNum = NodMat[i].CluNum; if (ModNum == DesNodNum || ModNum == ForNodNum) { NodMat[i].CluNum = DesNodNum,NodMat[i].LenOne = NewLenOne; NodMat[i].WidOne = NewWidOne,NodMat[i].LenTwo = NewLenTwo; NodMat[i].WidTwo = NewWidTwo,NodMat[i].ShDis = 100.0; NodMat[i].ShNum = 1000; } } // not understand why I add this statement and it works if (NodMat[ForNodNum].CluNum != DesNodNum) return -1; return 0; } // 計算聚類演算法準確率,這裡為了簡便,直接使用結果矩陣的最終的簇編號 // 有點投機取巧,需要使用者傳遞簇編號作為引數來判斷 // 引數:點結構矩陣,第一個簇編號,第二個簇編號,第三個簇編號 double AccRate(Node * NodMat, int ClOne, int ClTwo, int ClThree) { int Fa = 0; for(int i = 0; i < CPOne; i++) if (NodMat[i].CluNum != ClOne) Fa++; for (int i = CPOne; i < CPTwo; i++) if (NodMat[i].CluNum != ClTwo) Fa++; for (int i = CPTwo; i < ProLine; i++) if (NodMat[i].CluNum != ClThree) Fa++; return (1.00 - (double)Fa/ProLine); } // 根據層次聚類結果找到距離簇中心最近的點作為k-means演算法的初始簇 // 引數:層次聚類結果點矩陣,簇開頭結尾中心編號,初始簇編號,初始的點矩陣,儲存三個初始簇的點矩陣 void FinCenNod(Node *NodMat, int ClBeg, int ClEnd, int ClCenNum, int CenNum, Node *NodMatTwo, Node *NodMatCen) { double DIS, ShoDIS = 100.0; int num = 1000; Node *ClCen = (NodMat + ClCenNum); // 簇中心點 for (int i = ClBeg; i < ClEnd; i++) // 這裡利用同一個簇內點到該簇中心的距離 { DIS = DisCal((NodMatTwo+i),ClCen); if (ShoDIS > DIS) // 新的距離小於原最短距離 { ShoDIS = DIS; // 更新最短距離 num = i; // 更新離簇最近的點編號 } } ShoDIS = 100.0; // 儲存第一個簇中心的資訊 NodMatCen[CenNum].CluNum = NodMatTwo[num].CluNum, NodMatCen[CenNum].LenOne = NodMatTwo[num].LenOne; NodMatCen[CenNum].WidOne = NodMatTwo[num].WidOne, NodMatCen[CenNum].LenTwo = NodMatTwo[num].LenTwo; NodMatCen[CenNum].WidTwo = NodMatTwo[num].WidTwo; } // 重新計算簇中心的函式 // 引數:點結構矩陣,簇中心矩陣 void RecalCen(Node *NodMatTwo, Node *NodMatCen) { double NewLenOne, NewWidOne, NewLenTwo, NewWidTwo; NewLenOne = (NodMatTwo->LenOne + NodMatCen->LenOne) / 2; NewWidOne = (NodMatTwo->WidOne + NodMatCen->WidOne) / 2; NewLenTwo = (NodMatTwo->LenTwo + NodMatCen->LenTwo) / 2; NewWidTwo = (NodMatTwo->WidTwo + NodMatCen->WidTwo) / 2; NodMatCen->LenOne = NewLenOne; NodMatCen->WidOne = NewWidOne; NodMatCen->LenTwo = NewLenTwo; NodMatCen->WidTwo = NewWidTwo; } // 顯式資訊矩陣的資訊函式 // 引數:點結構矩陣,點數目 void ShowMat(Node * NodMat, int num) { for (int i = 0; i < num; i++) { std::cout << "Number: " << i << " : "; std::cout << NodMat[i].CluNum << ", " << NodMat[i].LenOne << ", " << NodMat[i].WidOne << ", " << NodMat[i].LenTwo << ", " << NodMat[i].WidTwo << ", " << NodMat[i].ShDis << ", " << NodMat[i].ShNum << std::endl; } } // k-means演算法,將點分配到最近的簇中並更新簇中心 // 引數:點結構矩陣,簇中心矩陣 void KMeans(Node * NodMatTwo, Node *NodMatCen) { double ShoDis = 100.0; // 儲存點到簇的最近的距離 int DesCluNum = 1000; // 儲存目標簇的編號 int j; for(int i = 0; i < ProLine; i++) { // 原來作為初始簇的三個點不重複加入 if (NodMatTwo[i].CluNum == NodMatCen[0].CluNum) { NodMatTwo[i].CluNum = 0; continue; } else if (NodMatTwo[i].CluNum == NodMatCen[1].CluNum) { NodMatTwo[i].CluNum = 1; continue; } else if (NodMatTwo[i].CluNum == NodMatCen[2].CluNum) { NodMatTwo[i].CluNum = 2; continue; } // 每個點都與三個簇計算歐式距離 for (j = 0; j < CentNum; j++) { double DIS = DisCal((NodMatTwo + i), (NodMatCen + j)); if(DIS < ShoDis) { ShoDis = DIS; DesCluNum = j; } } ShoDis = 100.0; // 重置最短距離 // 將點簇編號設定為最近的簇的編號 NodMatTwo[i].CluNum = DesCluNum; // 更新合併後簇的中心 RecalCen((NodMatTwo + i), (NodMatCen + DesCluNum)); } } int main(void) { using namespace std; ifstream DataStream; // 屬性檔案流 ifstream CheckStream; // 驗證檔案流 string FileLine; // 讀取檔案行存放的string double ProMat[ProLine][ProNum]; // 儲存四個屬性的矩陣 Node NodMat[ProLine]; // 層次聚類使用的點結構的矩陣 Node NodMatTwo[ProLine]; // k-means使用的點結構矩陣 Node NodMatCen[CentNum]; // 儲存k-means的簇中心的矩陣 const string SplitStr = ","; // CSV檔案分隔符 vector<string> LineWord; // 存放一行字串的容器 vector<double> ProValue; // 讀取的屬性值 vector<string> CheckWord; // 檢查的類別值 int PL = 0; // 屬性矩陣的行計數 int TotClu = 150; // 層次聚類中初始簇個數 // 下面的程式碼是進行前期資料的準備以執行相應的演算法 // 分別開啟兩個檔案並將檔案資訊儲存到相應資料結構中 DataStream.open("fisheriris_meas.csv", ios::in); if(!DataStream) // 讀取不成功則是NULL { cout << "Can't not open data file!\n"; return 0; } while(getline(DataStream, FileLine))// 每次讀取資料檔案的一行進行處理 { // 將檔案內容存入屬性矩陣 int column = 0; Split(FileLine, SplitStr, LineWord); for(auto val:LineWord) ProMat[PL][column++] = (double)atof(val.data()); PL++; } CheckStream.open("fisheriris_species.csv", ios::in); if(!CheckStream) // 讀取不成功則是NULL { cout << "Can't not open data file!\n"; return 0; } while(getline(CheckStream, FileLine))// 每次讀取資料檔案的一行進行處理 CheckWord.push_back(FileLine); // 將確定的類別放入容器中 NodMatGen(NodMat, ProMat), NodMatGen(NodMatTwo, ProMat);// 生成點矩陣 //執行演算法的程式碼 cout << "Begin to show Hierarchical Clustering result: " << endl; while(TotClu > 3) // 層次聚類演算法 { int MeSucc = 1; // 合併標誌,合併成功則總的簇數目-1 Node *Temp = FindSh(NodMat); // 找到具有最短距離的節點,同一個簇的最短距離相同 //cout << "To be Merge: " << Temp->CluNum << " Next: " << Temp->ShNum << endl; MeSucc = NodMer(Temp, NodMat); // 將簇合並 最終簇編號是最後一個具有最短距離的簇編號 if (!MeSucc) // 合併成功則總的簇數目-1 TotClu--; } ShowMat(NodMat, ProLine); cout << "Accuracy rate: " << AccRate(NodMat, 14, 50, 100) << endl; //k-means演算法 0, 50, 100 找到三個初始點作為簇中心 FinCenNod(NodMat, 0, CPOne, 0, 0, NodMatTwo, NodMatCen); FinCenNod(NodMat, CPOne, CPTwo, 50, 1, NodMatTwo, NodMatCen); FinCenNod(NodMat, CPTwo, ProLine, 100, 2, NodMatTwo, NodMatCen); ShowMat(NodMatCen, CentNum); KMeans(NodMatTwo, NodMatCen); cout << "Begin to show k-means Clustering result: " << endl; ShowMat(NodMatTwo, ProLine); cout << "Accuracy rate: " << AccRate(NodMatTwo, 0, 1, 2) << endl; return 0; }