• 一个Kmeans算法的C++实现


    其实这个程序是很早以前写的,只是这两天有人问这个算法,就把程序翻出来,加上了详细的注释。

    程序很简单,就是从一个observations.txt的文件中读取数据,并把数据对象划分成若干个聚类。

    具体格式:

    前3行分别是数据数量、数据维度和聚类数量
    后面每行为一个数据

    比如:

    9
    3
    4
     2.1  3.0 10.0
     4.0  5.2 -1.0
     5.1  1.5  2.3
    10.5 12.6 10.8
    12.1 10.9 11.0
     4.2  5.3 -9.8
     5.4  1.6  8.7
    -1.0 -2.1 -0.9
     0.5  0.3  0.4

    前三行分别表示有9个数据,数据的维度是3,分成是4个聚类。

    用这个数据运行后,程序打印出划分结果:

    ---- 第1个聚类 ----
    聚类中心:3.75,2.3,9.35
    2.1,3,10
    5.4,1.6,8.7

    ---- 第2个聚类 ----
    聚类中心:4.1,5.25,-5.4
    4,5.2,-1
    4.2,5.3,-9.8

    ---- 第3个聚类 ----
    聚类中心:1.53333,-0.1,0.6
    5.1,1.5,2.3
    -1,-2.1,-0.9
    0.5,0.3,0.4

    ---- 第4个聚类 ----
    聚类中心:11.3,11.75,10.9
    10.5,12.6,10.8
    12.1,10.9,11

    每个聚类的第一个数据是聚类中心,后面是划分到聚类中的数据。按这种划分,聚类的数据相互间最为接近。

    程序没有做任何优化,只是写出来帮初学者了解一下这个算法。


    #include <iostream>
    #include <fstream>
    #include <cmath>
    #include <cstdlib>
    #include <ctime>
    using namespace std;
    // 数据对象,size为维度
    struct Vector 
    {
      double* coords; // 所有维度的数值
      int     size;
      Vector() :  coords(0), size(0) {} 
      Vector(int d) { create(d); }
      // 创建维度为d的数据,并将各维度初始化为0
      void create(int d)
      {
        size = d;
        coords = new double[size];
        for (int i=0; i<size; i++)
          coords[i] = 0.0;
      }
      // 复制一个数据
      void copy(const Vector& other)
      {
        if (size == 0) // 如果原来没有数据,创建之
          create(other.size);
        
        for (int i=0; i<size; i++)
          coords[i] = other.coords[i];
      }
      // 将另一个数据的各个维度加在自身的维度上
      void add(const Vector& other)
      {
        for (int i=0; i<size; i++)
          coords[i] += other.coords[i];
      }
      // 释放数值的空间
      ~Vector()
      {
        if(coords)
          delete[] coords;
        size = 0;
      }
    };
    // 聚类结构
    struct Cluster 
    {
      Vector center;    // 中心/引力数据对象
      int*   member;    // 该聚类中各个数据的索引
      int    memberNum; // 数据的数量
    };

    // KMeans算法类
    class KMeans
    {
    private:
      int      num;          // 输入数据的数量
      int      dimen;        // 数据的维数
      int      clusterNum;   // 数据的聚类数
      Vector*  observations; // 所有数据存放在这个数组中
      Cluster* clusters;     // 聚类数组
      int      passNum;      // 迭代的趟数
    public:
      // 初始化参数和动态分配内存
      KMeans(int n, int d, int k, Vector* ob)
        : num(n)
        , dimen(d)
        , clusterNum(k)
        , observations(ob)
        , clusters(new Cluster[k])
      {
        for (int k=0; k<clusterNum; k++)
          clusters[k].member = new int[n];
      }
      // 释放内存
      ~KMeans()
      {
        for (int k=0; k<clusterNum; k++)
          delete [] clusters[k].member;
        delete [] clusters;
      }

      void initClusters()
      {
        // 由于初始数据中心是任意的,
        // 所以直接把前个数据作为NumClusters个聚类的数据中心
        for (int i=0; i<clusterNum; i++)
        {
          clusters[i].member[0] = i;                // 记录这个数据的索引到第i个聚类中
          clusters[i].center.copy(observations[i]); // 把这个数据作为数据中心
        }
      }
      void run()
      {
        bool converged = false; // 是否收敛
        passNum = 0;
        while (!converged && passNum < 999)   // 如果没有收敛,则再次迭代
                                            // 正常情况下总是会收敛,passNum < 999是防万一
        {
          distribute();                     // 将数据分配到聚中心最近的聚类
          converged = recalculateCenters(); // 计算新的聚类中心,如果计算结果和上次相同,认为已经收敛
          passNum++;
        }
      }
      void distribute()
      {
        // 将上次的记录的该聚类中的数据数量清0,重新开始分配数据
        for(int k=0; k<clusterNum; k++)
          getCluster(k).memberNum = 0;
        // 找出每个数据的最近聚类数据中心,并将该数据分配到该聚类
        for(int i=0; i<num; i++)
        {
          Cluster& cluster = getCluster(closestCluster(i)); // 找出最接近的其中心的聚类
          int memID = cluster.memberNum; // memberNum是当前记录的数据数量,也是新加入数据在member数组中的位置
          cluster.member[memID] = i;     // 将数据索引加入Member数组
          cluster.memberNum++;           // 聚类中的数据数量加1
        }
      }
      int closestCluster(int id)
      {
        int clusterID = 0;               // 暂时假定索引为id的数据最接近第一个聚类
        double minDist = eucNorm(id, 0); // 计算到第一个聚类中心的误差(本程序中用距离的平方和作为误差)
        // 计算其它聚类中心到数据的误差,找出其中最小的一个
        for (int k=1; k<clusterNum; k++) 
        {
          double d = eucNorm(id, k);
          if(d < minDist) // 如果小于前最小值,将改值作为当前最小值
          {
            minDist = d;
            clusterID = k;
          }
        }
        return clusterID;
      }
      // 索引为id的数据到第k个聚类中心的误差(距离的平方)
      double eucNorm(int id, int k)
      {
        Vector& observ = observations[id];
        Vector& center = clusters[k].center;
        double sumOfSquare = 0;
        // 将每个维度的差的平方相加,得到距离的平方
        for (int d=0; d<dimen; d++)
        {
          double dist = observ.coords[d] - center.coords[d]; // 在一个维度上中心到数据的距离
          sumOfSquare += dist*dist;
        }
        return sumOfSquare;
      }
      // 重新计算聚类中心
      bool recalculateCenters()
      {
        bool converged = true;
        
        for (int k=0; k<clusterNum; k++)
        {
          Cluster& cluster = getCluster(k);
          Vector average(dimen); // 初始的数据平均值
          // 统计这个聚类中数据的总和(因为在构造函数中会将各维数值清0,所以可以直接加)
          for (int m=0; m<cluster.memberNum; m++)
            average.add(observations[cluster.member[m]]);
          // 计算各个维度的评价值
          for(int d=0; d<dimen; d++)
          {
            average.coords[d] /= cluster.memberNum;
            if(average.coords[d] != cluster.center.coords[d]) // 如果和原来的聚类中心不同
                                                            // 表示没有收敛
            {
              converged = false;
              cluster.center.coords[d] = average.coords[d]; // 用这次的平均值作为新的聚类中心
            }
          }
        }
        return converged;
      }
      // 获得第id个聚类
      Cluster& getCluster(int id)
      {
        return clusters[id];
      }
    };
    // 打印一个数据
    void printVector(ostream& output, const Vector& v)
    {
      for (int i=0; i<v.size; i++)
      {
        if(i != 0)
          output << ",";
        output << v.coords[i];
      }
    }
    void partitionObservations(istream& input)
    {
      // 从input输入中获取数据
      int n, dimen, k;
      // 文本文件中头三个数据分别是数据数量(n)、数据维度(dimen)和聚类数量(k)
      input >> n >> dimen >> k;
      // 创建存储数据的数值
      Vector* obs = new Vector[n];
      // 将数据读入数组
      for (int i=0; i<n; i++)
      {
        obs[i].create(dimen); // 创建数据
        // 依次读入各个维度的数值
        for (int d=0; d<dimen; d++)
        {
          input >> obs[i].coords[d];
        }
      }
      // 建立KMeans算法类实例
      KMeans kmeans(n, dimen, k, obs);
      kmeans.initClusters(); // 初始化
      kmeans.run();          // 执行算法 
      
      // 输出聚类数据,如果希望输出到文件中,
      // 将后面的output的定义改为下面的形式即可
      // ofstream output("result.txt");
      ostream& output = cout;
      for (int c=0; c<k; c++)
      {
        Cluster& cluster = kmeans.getCluster(c);

        output << "---- 第" << (c + 1) << "个聚类 ----\n"// 显示第c个聚类

        output << "聚类中心:";
        printVector(output, cluster.center);

        for (int m=0; m<cluster.memberNum; m++)
        {
          int id = cluster.member[m];
          printVector(output, obs[id]);
          output << "\n";
        }
        output << endl;
      }
        delete[] obs;
    }
    int main()
    {
      const char* fileName = "observations.txt";
      ifstream obIn(fileName);
      if (obIn.is_open())
        partitionObservations(obIn);
      else
        cout << "open " << fileName << " is fail!" << endl;
      return 0;
    }

  • 相关阅读:
    Eclipse检出原MyEclipse项目后 javax.servlet.http相关类都报错【我,体现着一类jar包问题的处理方法】
    Tomcat中加载不到项目 项目构建Deployment Assembly报错:The given project is not a virtual component project
    Unity手游之路<十一>资源打包Assetbundle
    Unity手游之路<十二>手游资源热更新策略探讨
    Unity手游之路<十三>手游代码更新策略探讨
    观察者(Observer)模式
    访问者(Visitor)模式
    设计模式(15)-Facade Pattern
    使用Monitor调试Unity3D Android程序日志输出(非DDMS和ADB)
    思维导图快捷键
  • 原文地址:https://www.cnblogs.com/phoenixzq/p/2210219.html
Copyright © 2020-2023  润新知