K-均值聚類算法(k-means)的C++實現

K-均值聚類算法(k-means)的C++實現

k-均值聚類算法(k-means)主要用於解決 距離空間 上目標點集的自動分類問題。

本篇博文目的在於

  1. 闡述 k-means 聚類算法的數學模型
  2. 利用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(Pm,clusterCenteri(index))=min{d(Pm,clusterCenteri(j)),j=1,2,...,k}clusterAssignmenti+1(m)=indexclusterCenteri+1(n)=mean({PjclusterAssignmenti+1(j)=n}) d(P_m,clusterCenter_i(index))=min\{d(P_m,clusterCenter_i(j)),j=1,2,...,k\} \\ clusterAssignment_{i+1}(m)=index \\ clusterCenter_{i+1}(n)=mean(\{P_j|clusterAssignment_{i+1}(j)=n\})
其中 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 * dimensioncentroids[m] 代表第m類對象點的聚類中心。
成員變量 clusterAssment 則記錄了各個目標點所屬類。 clusterAssment[m].minIndex 爲第m目標點所屬類別, clusterAssment[m].minDist 爲第m目標點與其所屬類中心的距離。

KMEANS 類是k-means聚類算法本身,構造函數中不需要給定任何參數。
loadData 函數將待聚類數據 data 送入算法類中。其中需要說明: datavector< vector < T > > 型數據。類似於matlab-kmeans函數, data[m] 爲一個 vector< T > 型數據,代表着一個目標點的座標。可以認爲,data爲 particalCount * dimension 的矩陣。
kmeans 函數則是運行kmeans聚類算法,運行結果存入 centroidsclusterAssment 中。

KMEANS模塊的流如下圖所示:

loadData
kmeas
data
KMEANS類對象
clusterAssment
centroids

對應的函數調用:

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類中給出了一個默認的距離函數:
defaultDis(x,y)=(xy)(xy)=xy2 defaultDis(\vec x,\vec y) = (\vec x-\vec y) \bullet (\vec x-\vec y) = ||\vec x-\vec y||^2
需要注意,定義在空間中的距離函數不一定是經典的二範數的形式。距離函數可以由用戶給定,本文附上的KMEANS類代碼也留出了用戶指定距離函數的接口。用戶只需要重載KMEANS類,將用戶指定的距離函數送入 KMEANS::distEclud 距離函數指針即可。

代碼部分參考來源:
憶之獨秀的CSDN博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章