K-means聚類算法初探
1.1 算法初探
K-means是一種基於距離的迭代式算法。它將n個觀察實例分類到k個聚類中,以使得每個觀察實例距離它所在的聚類的中心點比其他的聚類中心點的距離更小。
其中,距離的計算方式可以是歐式距離(2-norm distance),或者是曼哈頓距離(Manhattan distance,1-norm distance)或者其他。這裏我們使用歐式距離。
要將每個觀測實例都歸類到距離它最近的聚類中心點,我們需要找到這些聚類中心點的具體位置。然而,要確定聚類中心點的位置,我們必須知道該聚類中包含了哪些觀測實例。這似乎是一個“先有蛋還是先有雞”的問題。從理論上來說,這是一個NP-Hard問題[3]。
但是,我們可以通過啓發式的算法來近似地解決問題。找到一個全局最優的方案是NP-hard問題,但是,降低問題的難度,如果能夠找到多個局部最優方案,然後通過一種評價其聚類結果優劣的方式,把其中最優的解決方案作爲結果,我們就可以得到一個不錯的聚類結果[2]。
這就是爲什麼我們說K-means算法是一個迭代式的算法。算法[2]的過程如下:
1)所有的觀測實例中隨機抽取出k個觀測點,作爲聚類中心點,然後遍歷其餘的觀測點找到距離各自最近的聚類中心點,將其加入到該聚類中。這樣,我們就有了一個初始的聚類結果,這是一次迭代的過程。
2)我們每個聚類中心都至少有一個觀測實例,這樣,我們可以求出每個聚類的中心點(means),作爲新的聚類中心,然後再遍歷所有的觀測點,找到距離其最近的中心點,加入到該聚類中。然後繼續運行2)。
3)如此往復2),直到前後兩次迭代得到的聚類中心點一模一樣。
這樣,算法就穩定了,這樣得到的k個聚類中心,和距離它們最近的觀測點構成k個聚類,就是我們要的結果。
實驗證明,算法試可以收斂的[2]。
計算聚類的中心點有三種方法如下:
1)Minkowski Distance 公式 —— λ 可以隨意取值,可以是負數,也可以是正數,或是無窮大。
公式(1)
2)Euclidean Distance 公式 —— 也就是第一個公式 λ=2 的情況
公式(2)
3)CityBlock Distance 公式 —— 也就是第一個公式 λ=1 的情況
公式(3)
這三個公式的求中心點有一些不一樣的地方,我們看下圖(對於第一個 λ 在 0-1之間)。
(1)Minkowski Distance (2)Euclidean Distance (3) CityBlock Distance
上面這幾個圖的大意是他們是怎麼個逼近中心的,第一個圖以星形的方式,第二個圖以同心圓的方式,第三個圖以菱形的方式。
那麼,如何評價一個聚類結果呢?我們計算所有觀測點距離它對應的聚類中心的距離的平方和即可,我們稱這個評價函數爲evaluate(C)。它越小,說明聚類越好。
1.2 K-means的問題及解決方案
K-means算法非常簡單,然而卻也有許多問題。
1)首先,算法只能找到局部最優的聚類,而不是全局最優的聚類。而且算法的結果非常依賴於初始隨機選擇的聚類中心的位置。我們通過多次運行算法,使用不同的隨機生成的聚類中心點運行算法,然後對各自結果C通過evaluate(C)函數進行評估,選擇多次結果中evaluate(C)值最小的那一個。
2)關於初始k值選擇的問題。首先的想法是,從一個起始值開始,到一個最大值,每一個值運行k-means算法聚類,通過一個評價函數計算出最好的一次聚類結果,這個k就是最優的k。我們首先想到了上面用到的evaluate(C)。然而,k越大,聚類中心越多,顯然每個觀測點距離其中心的距離的平方和會越小,這在實踐中也得到了驗證。第四節中的實驗結果分析中將詳細討論這個問題。
3)關於性能問題。原始的算法,每一次迭代都要計算每一個觀測點與所有聚類中心的距離。有沒有方法能夠提高效率呢?是有的,可以使用k-d tree或者ball tree這種數據結構來提高算法的效率。特定條件下,對於一定區域內的觀測點,無需遍歷每一個觀測點,就可以把這個區域內所有的點放到距離最近的一個聚類中去。這將在第三節中詳細地介紹。
基本Kmeans算法
1.選擇K個點作爲初始質心
2.repeat
3. 將每個點指派到最近的質心,形成K個簇
4. 重新計算每個簇的質心
5.until 簇不發生變化或達到最大迭代次數
時間複雜度:O(tKmn),其中,t爲迭代次數,K爲簇的數目,m爲記錄數,n爲維數
空間複雜度:O((m+K)n),其中,K爲簇的數目,m爲記錄數,n爲維數
#include <iostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <math.h>
#include <stdlib.h>
#define k 3//簇的數目
using namespace std;
//存放元組的屬性信息
typedef vector<double> Tuple;//存儲每條數據記錄
int dataNum;//數據集中數據記錄數目
int dimNum;//每條記錄的維數
//計算兩個元組間的歐幾裏距離
double getDistXY(const Tuple& t1, const Tuple& t2)
{
double sum = 0;
for(int i=1; i<=dimNum; ++i)
{
sum += (t1[i]-t2[i]) * (t1[i]-t2[i]);
}
return sqrt(sum);
}
//根據質心,決定當前元組屬於哪個簇
int clusterOfTuple(Tuple means[],const Tuple& tuple){
double dist=getDistXY(means[0],tuple);
double tmp;
int label=0;//標示屬於哪一個簇
for(int i=1;i<k;i++){
tmp=getDistXY(means[i],tuple);
if(tmp<dist) {dist=tmp;label=i;}
}
return label;
}
//獲得給定簇集的平方誤差
double getVar(vector<Tuple> clusters[],Tuple means[]){
double var = 0;
for (int i = 0; i < k; i++)
{
vector<Tuple> t = clusters[i];
for (int j = 0; j< t.size(); j++)
{
var += getDistXY(t[j],means[i]);
}
}
//cout<<"sum:"<<sum<<endl;
return var;
}
//獲得當前簇的均值(質心)
Tuple getMeans(const vector<Tuple>& cluster){
int num = cluster.size();
Tuple t(dimNum+1, 0);
for (int i = 0; i < num; i++)
{
for(int j=1; j<=dimNum; ++j)
{
t[j] += cluster[i][j];
}
}
for(int j=1; j<=dimNum; ++j)
t[j] /= num;
return t;
//cout<<"sum:"<<sum<<endl;
}
void print(const vector<Tuple> clusters[])
{
for(int lable=0; lable<k; lable++)
{
cout<<"第"<<lable+1<<"個簇:"<<endl;
vector<Tuple> t = clusters[lable];
for(int i=0; i<t.size(); i++)
{
cout<<i+1<<".(";
for(int j=0; j<=dimNum; ++j)
{
cout<<t[i][j]<<", ";
}
cout<<")\n";
}
}
}
void KMeans(vector<Tuple>& tuples){
vector<Tuple> clusters[k];//k個簇
Tuple means[k];//k箇中心點
int i=0;
//一開始隨機選取k條記錄的值作爲k個簇的質心(均值)
srand((unsigned int)time(NULL));
for(i=0;i<k;){
int iToSelect = rand()%tuples.size();
if(means[iToSelect].size() == 0)
{
for(int j=0; j<=dimNum; ++j)
{
means[i].push_back(tuples[iToSelect][j]);
}
++i;
}
}
int lable=0;
//根據默認的質心給簇賦值
for(i=0;i!=tuples.size();++i){
lable=clusterOfTuple(means,tuples[i]);
clusters[lable].push_back(tuples[i]);
}
double oldVar=-1;
double newVar=getVar(clusters,means);
cout<<"初始的的整體誤差平方和爲:"<<newVar<<endl;
int t = 0;
while(abs(newVar - oldVar) >= 1) //當新舊函數值相差不到1即準則函數值不發生明顯變化時,算法終止
{
cout<<"第 "<<++t<<" 次迭代開始:"<<endl;
for (i = 0; i < k; i++) //更新每個簇的中心點
{
means[i] = getMeans(clusters[i]);
}
oldVar = newVar;
newVar = getVar(clusters,means); //計算新的準則函數值
for (i = 0; i < k; i++) //清空每個簇
{
clusters[i].clear();
}
//根據新的質心獲得新的簇
for(i=0; i!=tuples.size(); ++i){
lable=clusterOfTuple(means,tuples[i]);
clusters[lable].push_back(tuples[i]);
}
cout<<"此次迭代之後的整體誤差平方和爲:"<<newVar<<endl;
}
cout<<"The result is:\n";
print(clusters);
}
int main(){
char fname[256];
cout<<"請輸入存放數據的文件名: ";
cin>>fname;
cout<<endl<<" 請依次輸入: 維數 樣本數目"<<endl;
cout<<endl<<" 維數dimNum: ";
cin>>dimNum;
cout<<endl<<" 樣本數目dataNum: ";
cin>>dataNum;
ifstream infile(fname);
if(!infile){
cout<<"不能打開輸入的文件"<<fname<<endl;
return 0;
}
vector<Tuple> tuples;
//從文件流中讀入數據
for(int i=0; i<dataNum && !infile.eof(); ++i)
{
string str;
getline(infile, str);
istringstream istr(str);
Tuple tuple(dimNum+1, 0);//第一個位置存放記錄編號,第2到dimNum+1個位置存放實際元素
tuple[0] = i+1;
for(int j=1; j<=dimNum; ++j)
{
istr>>tuple[j];
}
tuples.push_back(tuple);
}
cout<<endl<<"開始聚類"<<endl;
KMeans(tuples);
return 0;
}
參考:http://blog.csdn.net/qll125596718/article/details/8243404/
http://blog.csdn.net/skyline0623/article/details/8154911