聚类是指根据数据本身的特征对数据进行分类,不需要人工标注,是无监督学习的一种。k-means算法是聚类算法中最简单的算法之一。
k-means 算法将n个数据对象划分为 k个聚类以便使得所获得的聚类满足:同一聚类中的对象相似度较高;而不同聚类中的对象相似度较小。聚类相似度是利用各聚类中对象的均值所获得一个“中心对象”(引力中心)来进行计算的。
基于这样一个假设,我们再来导出k-means所要优化的目标函数:设我们一共有N个数据点需要分为K个cluster,而k-means要做的就是要最小化这个目标函数
为第k个类聚中心,当第n个数据属于第k类时为1,否则为0。
过程如下:
1.首先从n个数据对象任意选择 k 个对象作为初始聚类中心;而对于所剩下其它对象,则根据它们与这些聚类中心的相似度(距离),分别将它们分配给与其最相似的(聚类中心所代表的)聚类;
2.然后再计算每个所获新聚类的聚类中心(该聚类中所有对象的均值);不断重复这一过程直到标准测度函数开始收敛为止。
一般都采用均方差作为标准测度函数,k个聚类具有以下特点:各聚类本身尽可能的紧凑,而各聚类之间尽可能的分开。
每一次更新聚类中心都会使目标函数减小,因此迭代最终J会达到一个极小值,不能保证是全局最小值。k-means对于噪声十分敏感。
c++实现:
class ClusterMethod
{
private:
double **mpSample;//输入
double **mpCenters;//储存聚类中心
double **pDistances;//距离矩阵
int mSampleNum;//样本数
int mClusterNum;//聚类数
int mFeatureNum;//每个样本特征数
int *ClusterResult;//聚类结果
int MaxIterationTimes;//最大迭代次数
public:
void GetClusterd(vector<std::vector<std::vector<double> > >&v, double** feateres, int ClusterNum, int SampleNum, int FeatureNum);//外部接口
private:
void Initialize(double** feateres, int ClusterNum, int SampleNum, int FeatureNum);//类初始化
void k_means(vector<vector<vector<double> > >&v);//算法入口
void k_means_Initialize();//聚类中心初始化
void k_means_Calculate(vector<vector<vector<double> > >&v);//聚类计算
};
类内函数实现:
//param@v 保存分类结果 v[i][j][k]表示第i类第j个数据第k个特征(从0开始)
//param@feateres 输入数据 feateres[i][j]表示第i个数据第j个特征(i,j从0开始)
//param@ClusterNum 分类数
//param@SampleNum 数据数量
//param@FeatureNum 数据特征数
void ClusterMethod::GetClusterd(vector<std::vector<std::vector<double> > >&v, double** feateres, int ClusterNum, int SampleNum, int FeatureNum)
{
Initialize(feateres, ClusterNum, SampleNum, FeatureNum);
k_means(v);
}
//类内数据初始化
void ClusterMethod::Initialize(double** feateres, int ClusterNum, int SampleNum, int FeatureNum)
{
mpSample = feateres;
mFeatureNum = FeatureNum;
mSampleNum = SampleNum;
mClusterNum = ClusterNum;
MaxIterationTimes = 50;
mpCenters = new double*[mClusterNum];
for (int i = 0; i < mClusterNum; ++i)
{
mpCenters[i] = new double[mFeatureNum];
}
pDistances = new double*[mSampleNum];
for (int i = 0; i < mSampleNum; ++i)
{
pDistances[i] = new double[mClusterNum];
}
ClusterResult = new int[mSampleNum];
}
//算法入口
void ClusterMethod::k_means(vector<vector<vector<double> > >&v)
{
k_means_Initialize();
k_means_Calculate(v);
}
//初始化聚类中心
void ClusterMethod::k_means_Initialize()
{
for (int i = 0; i < mClusterNum; ++i)
{
//mpCenters[i] = mpSample[i];
for (int k = 0; k < mFeatureNum; ++k)
{
mpCenters[i][k] = mpSample[i][k];
}
}
}
上面初始化聚类中心是令数据前i(i为聚类中心个数)个点为i个聚类中心。(注意一定不能使用mpCenters[i] = mpSample[i]进行初始化,它们是指针。)
也可以随机选取i个数据令为聚类中心,这样同样的数据多次的运行结果就可能不同。因为k-means结果不一定达到全局最小点,最简单的解决方法就是多次运行(这里指整个函数的重复运行,与聚类时的迭代次数不同)取目标函数最小时候的聚类结果。如果像前面那种每次都选前i个数据初始化聚类中心,多次运行将不能解决局部最小点问题。
聚类和更新聚类中心的实现如下:
//聚类过程
void ClusterMethod::k_means_Calculate(vector<vector<vector<double> > >&v)
{
double J = DBL_MAX;//目标函数
int time = MaxIterationTimes;
while (time)
{
double now_J = 0;//上次更新距离中心后的目标函数
--time;
//距离初始化
for (int i = 0; i < mSampleNum; ++i)
{
for (int j = 0; j < mClusterNum; ++j)
{
pDistances[i][j] = 0;
}
}
//计算欧式距离
for (int i = 0; i < mSampleNum; ++i)
{
for (int j = 0; j < mClusterNum; ++j)
{
for (int k = 0; k < mFeatureNum; ++k)
{
pDistances[i][j] += abs(pow(mpSample[i][k], 2) - pow(mpCenters[j][k], 2));
}
now_J += pDistances[i][j];
}
}
if (J - now_J < 0.01)//目标函数不再变化结束循环
{
break;
}
J = now_J;
//a存放临时分类结果
vector<vector<vector<double> > > a(mClusterNum);
for (int i = 0; i < mSampleNum; ++i)
{
double min = DBL_MAX;
for (int j = 0; j < mClusterNum; ++j)
{
if (pDistances[i][j] < min)
{
min = pDistances[i][j];
ClusterResult[i] = j;
}
}
vector<double> vec(mFeatureNum);
for (int k = 0; k < mFeatureNum; ++k)
{
vec[k] = mpSample[i][k];
}
a[ClusterResult[i]].push_back(vec);
// v[ClusterResult[i]].push_back(vec);这里不能这样给v输入数据,因为v没有初始化大小
}
v = a;
//计算新的聚类中心
for (int j = 0; j < mClusterNum; ++j)
{
for (int k = 0; k < mFeatureNum; ++k)
{
mpCenters[j][k] = 0;
}
}
for (int j = 0; j < mClusterNum; ++j)
{
for (int k = 0; k < mFeatureNum; ++k)
{
for (int s = 0; s < v[j].size(); ++s)
{
mpCenters[j][k] += v[j][s][k];
}
if (v[j].size() != 0)
{
mpCenters[j][k] /= v[j].size();
}
}
}
}
//输出聚类中心
for (int j = 0; j < mClusterNum; ++j)
{
for (int k = 0; k < mFeatureNum; ++k)
{
cout << mpCenters[j][k] << " ";
}
cout << endl;
}
}
生成随机数据函数:
//param@datanum 数据数量
//param@featurenum 每个数据特征数
double** createdata(int datanum, int featurenum)
{
srand((int)time(0));
double** data = new double*[datanum];
for (int i = 0; i < datanum; ++i)
{
data[i] = new double[featurenum];
}
cout << "输入数据:" << endl;
for (int i = 0; i < datanum ; ++i)
{
for (int j = 0; j < featurenum; ++j)
{
data[i][j] = ((int)rand() % 30) / 10.0;
cout << data[i][j] << " ";
}
cout << endl;
}
return data;
}
主函数:
int main()
{
vector<std::vector<std::vector<double> > >v;
double** data = createdata(10, 2);
ClusterMethod a;
a.GetClusterd(v, data, 3, 10, 2);
for (int i = 0; i < v.size(); ++i)
{
cout << "第" << i+1 << "类" << endl;
for (int j = 0; j < v[i].size(); ++j)
{
for (int k = 0; k < v[i][j].size(); ++k)
{
cout << v[i][j][k] << " ";
}
cout << endl;
}
}
}
运行结果如下:
算法改进
k-means更新聚类中心时使用每类的均值,因此对噪声较为敏感。
k-medoids将前面的均值改为中位数,这样可以避免个别过大或过小的点对聚类中心的影响。
说到中位数,我就想起了唐僧师徒。。。不对,想起了之前写的选择算法,不需要对所有数据进行排序,运行速度更快。
但这也不是说k-medoids就一定比k-means好,要知道,数据量较大时,算法的时间也是关键。求均值所需时间比求中位数小。因此,孰优孰劣需要看具体需求。
来源:CSDN
作者:红鱼鱼
链接:https://blog.csdn.net/qq_40692109/article/details/103863675