層次聚類以及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;
}