K-均值聚類算法(k-means)的C++實現
k-均值聚類算法(k-means)主要用於解決 距離空間 上目標點集的自動分類問題。
本篇博文目的在於
- 闡述 k-means 聚類算法的數學模型
- 利用C++改寫並封裝K-Means算法,接口數據向MATLAB中kmeans函數看齊。
k-means算法的數學原理
k-means聚類算法的問題描述如下:
假設我們的研究對象可以一一映射到 dimension 維歐式空間中的一個點 P 。現在我們需要把空間中相近的目標點圈起來,看作是一類對象,一共需要分爲 k 類。
先來介紹k-means算法涉及到的數據結構:
struct kmeans
{
double clusterCenter[k][dimension];
int clusterAssignment[targetCount];
}
clusterCenter[m] 代表第m類對象的類中心,爲 dimension 維的向量,共有k個這樣的聚類中心。
clusterAssignment[m] 則記錄了目標集合中第m個目標點所屬的類。
k-means算法是一個迭代算法,迭代的數學公式如下:
其中 d 爲定義在目標所在空間上的距離函數。i爲迭代次數,m,n,j爲下標變量。
用一般語言對迭代過程進行描述:對於任一目標點 P,設距離其最近的聚類中心爲類 C 中心,把目標點 P 歸於類別 C 。完成所有目標點的歸類後,將同一類的目標點求其質心,作爲該類的新的聚類中心。重複上述操作,直至所有目標點的分類不再發生改變。
k-means算法的c++實現
對於k-means算法的c++代碼已經比較常見,本文附上的C++代碼則更關注於將k-means算法進行封裝,提高算法模塊的獨立性,便於再次開發,向MATLAB中的kmeans函數看齊。
本節所提供的c++代碼主要在CSDN博主—— 憶之獨秀 的博文代碼基礎上進行修改與封裝。原文請見鏈接:
憶之獨秀的CSDN博客
引用博主的僞代碼對算法實現核心進行描述:
創建k個點作爲起始質心(經常是隨機選擇)
當任意一個點的簇分配結果發生改變時
對數據集中的每個數據點
對每個質心
計算質心與數據點之間的距離
將數據點分配到距其最近的簇
對每一個簇,計算簇中所有點的均值並將均值作爲質心
以下給出封裝後的KMEANS算法類中常用的幾個變量與函數的解釋。
template<typename T>
//cluster centers
vector< vector<T> > centroids;
//mark which cluster the data belong to
vector<tNode> clusterAssment;
//construct function
KMEANS::KMEANS(void);
//load data into dataSet
void KMEANS::loadData(vector< vector<T> > data);
//running the kmeans algorithm
void KMEANS::kmeans(int clusterCount);
成員變量 centroids 爲聚類中心,規模爲 k * dimension , centroids[m] 代表第m類對象點的聚類中心。
成員變量 clusterAssment 則記錄了各個目標點所屬類。 clusterAssment[m].minIndex 爲第m目標點所屬類別, clusterAssment[m].minDist 爲第m目標點與其所屬類中心的距離。
KMEANS 類是k-means聚類算法本身,構造函數中不需要給定任何參數。
loadData 函數將待聚類數據 data 送入算法類中。其中需要說明: data 爲 vector< vector < T > > 型數據。類似於matlab-kmeans函數, data[m] 爲一個 vector< T > 型數據,代表着一個目標點的座標。可以認爲,data爲 particalCount * dimension 的矩陣。
kmeans 函數則是運行kmeans聚類算法,運行結果存入 centroids 與 clusterAssment 中。
KMEANS模塊的流如下圖所示:
對應的函數調用:
KMEANS<double> YKmeans;
YKmeans.loadData(data);
YKmeans.kmeans();
//use YKmeans.Assment and YKmeans.centroids
以下附上KMEANS類的頭文件與對應的例程代碼。
測試範例:main.cpp
#include "YKmeans.h"
#include <vector>
#include <iostream>
#include <math.h>
using namespace std;
const int pointsCount = 9;
const int clusterCount = 2;
int main()
{
//構造待聚類數據集
vector< vector<double> > data;
vector<double> points[pointsCount];
for (int i = 0; i < pointsCount; i++)
{
points[i].push_back(i);
points[i].push_back(i*i);
points[i].push_back(sqrt(i));
data.push_back(points[i]);
}
//構建聚類算法
KMEANS<double> kmeans;
//數據加載入算法
kmeans.loadData(data);
//運行k均值聚類算法
kmeans.kmeans(clusterCount);
//輸出聚類後各點所屬類情況
for (int i = 0; i < pointsCount; i++)
cout << kmeans.clusterAssment[i].minIndex << endl;
//輸出類中心
cout << endl << endl;
for (int i = 0; i < clusterCount; i++)
{
for (int j = 0; j < 3; j++)
cout << kmeans.centroids[i][j] << ',' << '\t' ;
cout << endl;
}
return(0);
}
KMEANS類對應的頭文件:YKmeans.h
#ifndef _YKMEANS_H_
#define _YKMEANS_H_
#include <cstdlib> //for rand()
#include <vector> //for vector<>
#include <time.h> //for srand
#include <limits.h> //for INT_MIN INT_MAX
using namespace std;
template<typename T>
class KMEANS
{
protected:
//colLen:the dimension of vector;rowLen:the number of vectors
int colLen, rowLen;
//count to be clustered
int k;
//mark the min and max value of a array
typedef struct MinMax
{
T Min;
T Max;
MinMax(T min, T max) :Min(min), Max(max) {}
}tMinMax;
//distance function
//reload this function if necessary
double (*distEclud)(vector<T> &v1, vector<T> &v2);
//get the min and max value in idx-dimension of dataSet
tMinMax getMinMax(int idx)
{
T min, max;
dataSet[0].at(idx) > dataSet[1].at(idx) ? (max = dataSet[0].at(idx), min = dataSet[1].at(idx)) : (max = dataSet[1].at(idx), min = dataSet[0].at(idx));
for (int i = 2; i < rowLen; i++)
{
if (dataSet[i].at(idx) < min) min = dataSet[i].at(idx);
else if (dataSet[i].at(idx) > max) max = dataSet[i].at(idx);
else continue;
}
tMinMax tminmax(min, max);
return tminmax;
}
//generate clusterCount centers randomly
void randCent(int clusterCount)
{
this->k = clusterCount;
//init centroids
centroids.clear();
vector<T> vec(colLen, 0);
for (int i = 0; i < k; i++)
centroids.push_back(vec);
//set values by column
srand(time(NULL));
for (int j = 0; j < colLen; j++)
{
tMinMax tminmax = getMinMax(j);
T rangeIdx = tminmax.Max - tminmax.Min;
for (int i = 0; i < k; i++)
{
/* generate float data between 0 and 1 */
centroids[i].at(j) = tminmax.Min + rangeIdx * (rand() / (double)RAND_MAX);
}
}
}
//default distance function ,defined as dis = (x-y)'*(x-y)
static double defaultDistEclud(vector<T> &v1, vector<T> &v2)
{
double sum = 0;
int size = v1.size();
for (int i = 0; i < size; i++)
{
sum += (v1[i] - v2[i])*(v1[i] - v2[i]);
}
return sum;
}
public:
typedef struct Node
{
int minIndex; //the index of each node
double minDist;
Node(int idx, double dist) :minIndex(idx), minDist(dist) {}
}tNode;
KMEANS(void)
{
k = 0;
colLen = 0;
rowLen = 0;
distEclud = defaultDistEclud;
}
~KMEANS(void){}
//data to be clustered
vector< vector<T> > dataSet;
//cluster centers
vector< vector<T> > centroids;
//mark which cluster the data belong to
vector<tNode> clusterAssment;
//load data into dataSet
void loadData(vector< vector<T> > data)
{
this->dataSet = data; //kmeans do not change the original data;
this->rowLen = data.capacity();
this->colLen = data.at(0).capacity();
}
//running the kmeans algorithm
void kmeans(int clusterCount)
{
this->k = clusterCount;
//initial clusterAssment
this->clusterAssment.clear();
tNode node(-1, -1);
for (int i = 0; i < rowLen; i++)
clusterAssment.push_back(node);
//initial cluster center
this->randCent(clusterCount);
bool clusterChanged = true;
//the termination condition can also be the loops less than some number such as 1000
while (clusterChanged)
{
clusterChanged = false;
for (int i = 0; i < rowLen; i++)
{
int minIndex = -1;
double minDist = INT_MAX;
for (int j = 0; j < k; j++)
{
double distJI = distEclud(centroids[j], dataSet[i]);
if (distJI < minDist)
{
minDist = distJI;
minIndex = j;
}
}
if (clusterAssment[i].minIndex != minIndex)
{
clusterChanged = true;
clusterAssment[i].minIndex = minIndex;
clusterAssment[i].minDist = minDist;
}
}
//step two : update the centroids
for (int cent = 0; cent < k; cent++)
{
vector<T> vec(colLen, 0);
int cnt = 0;
for (int i = 0; i < rowLen; i++)
{
if (clusterAssment[i].minIndex == cent)
{
++cnt;
//sum of two vectors
for (int j = 0; j < colLen; j++)
{
vec[j] += dataSet[i].at(j);
}
}
}
//mean of the vector and update the centroids[cent]
for (int i = 0; i < colLen; i++)
{
if (cnt != 0) vec[i] /= cnt;
centroids[cent].at(i) = vec[i];
}
}
}
}
};
#endif // _YKMEANS_H_
後注
在本文給出的KMEANS類中給出了一個默認的距離函數:
需要注意,定義在空間中的距離函數不一定是經典的二範數的形式。距離函數可以由用戶給定,本文附上的KMEANS類代碼也留出了用戶指定距離函數的接口。用戶只需要重載KMEANS類,將用戶指定的距離函數送入 KMEANS::distEclud 距離函數指針即可。
代碼部分參考來源:
憶之獨秀的CSDN博客