K-means聚類算法原理和C++實現


給定訓練集$\{x^{(1)},...,x^{(m)}\}$,想把這些樣本分成不同的子集,即聚類,$x^{(i)}\in\mathbb{R^{n}}$,但是這是個無標簽數據集,也就是說我們再聚類的時候不能利用標簽信息,所以這是一個無監督學習問題。

k-means聚類算法的流程如下:

1. 隨機初始化聚類中心$\mu_{1},\mu_{2},...,\mu_{k}\in\mathbb{R}^{n}$

2. a. 對與每一個聚類中心,計算所有樣本到該聚類中心的距離,然后選出距離該聚類中心最近的幾個樣本作為一類;

  $c^{(i)}:=\arg\min_{j}||x^{(i)}-\mu_{j}||^{2}$

 

  這個公式的意思是,某個樣本 i 屬於哪一類,取決於該樣本距離哪一個聚類中心最近,步驟a就是利用這個規則實現。 

  b. 對上面分成的k類,根據類里面的樣本,重新估計該類的中心:

   $\mu_{j}:=\frac{\sum_{i=1}^{m}1\{c^{(i)}=j\}x^{(j)}}{\sum_{i=1}^{m}1\{c^{(i)}=j\}}$

  對於新的聚類中心,重復a,這里1{...}是一個真值判斷,例如1{3=2}=0,1{3=3}=1.

  

  c. 重復a和b直至收斂

但是k-means真的能保證收斂嗎?k-means的目的是選出聚類中心和每一類的樣本,定義失真函數:

$J(c,\mu)=\sum_{i=1}^{m}||x^{(i)}-\mu_{c^{(i)}}||^{2}$

 

這個函數衡量的是某個聚類的中心與該類中所有樣本距離的平方和,根據上面k-means的算法,可以看出,a 是固定聚類中心,選擇該類的樣本,b 是樣本固定,調整聚類中心,即每次都是固定一個變量,調整另一個變量,所以k-means完全是在針對失真函數 J 坐標上升,這樣,J 必然是單調遞減,所以J的值必然收斂。在理論上,這種方法可能會使得k-means在一些聚類結果之間產生震盪,即幾組不同的 c 和 μ 有着相同的失真函數值,但是這種情況在實際情況中很少出現。

由於失真函數是一個非凸函數,所以坐標上升不能保證該函數全局收斂,即失真函數容易陷入局部收斂。但是大多數情況下,k-means都可以產生不錯的結果,如果擔心陷入局部收斂,可以多運行幾次k-means(采用不同的隨機初始聚類中心),然后從多次結果中選出失真函數最小的聚類結果。

 

下面是一個簡單k-means的C++代碼,對{1, 2, 3, 11, 12, 13, 21, 22, 23}這9個樣本值聚類:

  1 #include<iostream>
  2 #include<cmath>
  3 #include<vector>
  4 #include<ctime>
  5 using namespace std;
  6 typedef unsigned int uint;
  7 
  8 struct Cluster
  9 {
 10     vector<double> centroid;
 11     vector<uint> samples;
 12 };
 13 double cal_distance(vector<double> a, vector<double> b)
 14 {
 15     uint da = a.size();
 16     uint db = b.size();
 17     if (da != db) cerr << "Dimensions of two vectors must be same!!\n";
 18     double val = 0.0;
 19     for (uint i = 0; i < da; i++)
 20     {
 21         val += pow((a[i] - b[i]), 2);
 22     }
 23     return pow(val, 0.5);
 24 }
 25 vector<Cluster> k_means(vector<vector<double> > trainX, uint k, uint maxepoches)
 26 {
 27     const uint row_num = trainX.size();
 28     const uint col_num = trainX[0].size();
 29 
 30     /*初始化聚類中心*/
 31     vector<Cluster> clusters(k);
 32     uint seed = (uint)time(NULL); 
33
for (uint i = 0; i < k; i++) 34 { 35 srand(seed); 36 int c = rand() % row_num; 37 clusters[i].centroid = trainX[c]; 38 seed = rand(); 39 } 40 41 /*多次迭代直至收斂,本次試驗迭代100次*/ 42 for (uint it = 0; it < maxepoches; it++) 43 { 44 /*每一次重新計算樣本點所屬類別之前,清空原來樣本點信息*/ 45 for (uint i = 0; i < k; i++) 46 { 47 clusters[i].samples.clear(); 48 } 49 /*求出每個樣本點距應該屬於哪一個聚類*/ 50 for (uint j = 0; j < row_num; j++) 51 { 52 /*都初始化屬於第0個聚類*/ 53 uint c = 0; 54 double min_distance = cal_distance(trainX[j],clusters[c].centroid); 55 for (uint i = 1; i < k; i++) 56 { 57 double distance = cal_distance(trainX[j], clusters[i].centroid); 58 if (distance < min_distance) 59 { 60 min_distance = distance; 61 c = i; 62 } 63 } 64 clusters[c].samples.push_back(j); 65 } 66 67 /*更新聚類中心*/ 68 for (uint i = 0; i < k; i++) 69 { 70 vector<double> val(col_num, 0.0); 71 for (uint j = 0; j < clusters[i].samples.size(); j++) 72 { 73 uint sample = clusters[i].samples[j]; 74 for (uint d = 0; d < col_num; d++) 75 { 76 val[d] += trainX[sample][d]; 77 if (j == clusters[i].samples.size() - 1) 78 clusters[i].centroid[d] = val[d] / clusters[i].samples.size(); 79 } 80 } 81 } 82 } 83 return clusters; 84 } 85 86 int main() 87 { 88 vector<vector<double> > trainX(9,vector<double>(1,0)); 89 //對9個數據{1 2 3 11 12 13 21 22 23}聚類 90 double data = 1.0; 91 for (uint i = 0; i < 9; i++) 92 { 93 trainX[i][0] = data; 94 if ((i+1) % 3 == 0) data += 8; 95 else data++; 96 } 97 98 /*k-means聚類*/ 99 vector<Cluster> clusters_out = k_means(trainX, 3, 100); 100 101 /*輸出分類結果*/ 102 for (uint i = 0; i < clusters_out.size(); i++) 103 { 104 cout << "Cluster " << i << " :" << endl; 105 106 /*子類中心*/ 107 cout << "\t" << "Centroid: " << "\n\t\t[ "; 108 for (uint j = 0; j < clusters_out[i].centroid.size(); j++) 109 { 110 cout << clusters_out[i].centroid[j] << " "; 111 } 112 cout << "]" << endl; 113 114 /*子類樣本點*/ 115 cout << "\t" << "Samples:\n"; 116 for (uint k = 0; k < clusters_out[i].samples.size(); k++) 117 { 118 uint c = clusters_out[i].samples[k]; 119 cout << "\t\t[ "; 120 for (uint m = 0; m < trainX[0].size(); m++) 121 { 122 cout << trainX[c][m] << " "; 123 } 124 cout << "]\n"; 125 } 126 } 127 return 0; 128 }

 

下面是4次運行結果:

由於數據簡單,容易看出第一次和第是三次結果是理想的,而第二次和第四次都是較差出的聚類結果,即上面說的失真函數陷入了局部最優,所以在實踐中多次運行,取出較好的聚類結果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM