AGNES聚类算法的理解与应用

与前文介绍的DBSCAN聚类算法类似,AGNES算法也属于无监督的数据分类算法。更细地划分,该算法属于自底向上的层次聚类算法。该算法的核心思路是,首先设定一个期望的分类数目n,一开始把每个数据样本都分别看成一个类,然后计算所有类之间两两的距离,找出距离最短的两个类,并把这两个类合并为一个类,到此则总类数减1。接着再重复上述过程:计算所有类之间两两的距离,找出距离最短的两个类,并把这两个类合并为一个类。以此类推,类总数逐渐减少,直到类总数减少到n为止,则停止分类。

下面举一个简单的例子来说明AGNES算法的过程,假设有A、B、C、D、E、F这6个数据样本,要把他们分为3类,也即n=3,那么分类过程如下。

首先,把A、B、C、D、E、F都分别看成一个类,计算它们两两的距离:


A
B
C
D
E
F
A
d00
d01
d02
d03
d04
d05
B
d10
d11
d12d13
d14
d15
C
d20
d21
d22d23d24
d25
D
d30
d31
d32d33d34
d35
E
d40
d41
d42d43
d44d45
F
d50
d51
d52d53
d54
d55

上表中的所有距离,我们称之为该算法的距离矩阵,显然,该矩阵是一个对称矩阵。假设矩阵中d02与d20的距离最小,也即A与C的距离最短,那么将A与C合并为一个类,类总数由6减少为5:AC、B、D、E、F。然后再计算这5类的两两距离:


AC
B
D
E
F
AC
d00
d01
d02
d03
d04
B
d10
d11
d12d13
d14
D
d20
d21
d22d23d24
E
d30
d31
d32d33d34
F
d40
d41
d42d43
d44

上表中,假设矩阵中d32与d23的距离最小,也即D与E的距离最短,那么将D与E合并为一个类,类总数由5减少为4:AC、B、DE、F。然后再计算这4类的两两距离:


AC
B
DE
F
AC
d00
d01
d02
d03
B
d10
d11
d12d13
DE
d20
d21
d22d23
F
d30
d31
d32d33

上表中,假设矩阵中d01与d10的距离最小,也即AC与B的距离最短,那么将AC与B合并为一个类,类总数由4减少为3:ACB、DE、F。至此,分类结束。

类与类的距离的计算,有多种方法,下面举例说明各个计算方法,假设有两个类{A, B}和{C, D},第一个类含有样本A、B,第二个类含有样本C、D。

1. 取两个类中各样本距离(欧式距离)的最大值:

d = max(d(A,C), d(A,D), d(B,C), d(B,D))

2. 取两个类中各样本距离的最小值:

d = min(d(A,C), d(A,D), d(B,C), d(B,D))

3. 取两个类中各样本距离的平均值:

d =(d(A,C) + d(A,D) + d(B,C) + d(B,D))/4

本文中,我们不使用以上方法来计算类的距离,我们使用类的中心点距离作为类距离,这样可以简化计算:

d = d((A+B)/2, (C+D)/2)

下面,我们使用C++来实现AGNES算法,对一组点进行分类,每个点都有x坐标与y坐标,因此每个点相当于一个二维向量。

定义点的类:

class point
{
  public:
    float x;    //x坐标
    float y;    //y坐标
    point()
    {
      x = 0;
      y = 0;
    }
};

计算两个类的距离:

float cal_cluster_distance(vector<point> p1, vector<point> p2)
{
  point m1;
  for(int i = 0; i < p1.size(); i++)
  {
    m1.x += p1[i].x;
    m1.y += p1[i].y;
  }


  m1.x /= p1.size();
  m1.y /= p1.size();


  point m2;
  for(int i = 0; i < p2.size(); i++)
  {
    m2.x += p2[i].x;
    m2.y += p2[i].y;
  }


  m2.x /= p2.size();
  m2.y /= p2.size();


  float d = sqrt((m1.x-m2.x)*(m1.x-m2.x) + (m1.y-m2.y)*(m1.y-m2.y));
  return d;
}

计算距离矩阵:

/*
例如:
      (A,B) (C,D) (E,F)
(A,B)   0     d1    d2
(C,D)   d3    0     d4
(E,F)   d5    d6    0
*/
vector<vector<float>> cal_distance_array(vector<vector<point>> p)
{
  vector<vector<float>> d;


  for(int i = 0; i < p.size(); i++)   //计算类集合中所有簇的两两之间的簇距离,组成距离矩阵
  {
    vector<float> tmp;
    for(int j = 0; j < p.size(); j++)
    {
      //如果不是同一个类,则计算不同两个类的中心点的距离作为类的距离
      {
        float dd = cal_cluster_distance(p[i], p[j]);
        tmp.push_back(dd);
      }
      else   //如果是同一个类,则距离为0 
      {
        tmp.push_back(0);
      }
    }
    d.push_back(tmp);
  }


  return d;
}

求距离矩阵中最近的类的索引:

void get_min_point(vector<vector<float>> M, int &idx_i, int &idx_j)
{
  float min = 999999999.9999;


  for(int i = 0; i < M.size(); i++)    //求距离矩阵中的非零最小值
  {
    for(int j = 0; j < M[i].size(); j++)
    {
      if(min > M[i][j] && M[i][j] != 0)
        min = M[i][j];
    }
  }


  for(int i = 0; i < M.size(); i++)    //求非零最小值的两个簇的索引
  {
    for(int j = 0; j < M[i].size(); j++)
    {
      if(min == M[i][j])
      {
        if(i < j)    //索引较小的簇在簇集合中位于较前面的位置,把索引较大的簇合并到索引较小的簇,当删除索引较大的簇之后,并不影响索引较小簇的索引值,方便重新计算合并后簇的距离
        {
          idx_i = i;
          idx_j = j;
        }
        else
        {
          idx_i = j;
          idx_j = i;
        }
        return;
      }
        
    }
  }
}

删除距离矩阵的第idx行和第idx列:

void delete_row_col(vector<vector<float>> &d, int idx)
{
  d.erase(d.begin()+idx);   //删除第idx行
  for(int i = 0; i < d.size(); i++)   //删除第idx列
  {
    d[i].erase(d[i].begin()+idx);
  }
}

将类B合并到类A的尾端,并将类B删除:

void combine_cluster(vector<vector<point>> &p, int A_idx, int B_idx)
{
  p[A_idx].insert(p[A_idx].end(), p[B_idx].begin(), p[B_idx].end());   //将簇B合并到簇A
  p.erase(p.begin()+B_idx);     //删除类B
}

下面上正餐,AGNES算法计算实现:

vector<vector<point>> Agnes(vector<point> p, int cluster_num)
{
  vector<vector<point>> pp;
  for(int i = 0; i < p.size(); i++)    //初始化每一个点为一个类
  {
    vector<point> tmp;
    tmp.push_back(p[i]);
    pp.push_back(tmp);
  }


  vector<vector<float>> M = cal_distance_array(pp);   //计算类集合的距离矩阵


  int cnt = p.size();
  int ii, jj;
  while(cnt > cluster_num)
  {
    get_min_point(M, ii, jj);  //获取距离矩阵中距离为非零最小值的两个类的索引,ii为较小索引,jj为较大索引
    combine_cluster(pp, ii, jj);  //将索引为jj的类合并到索引为ii的类,并删除索引为jj的类
    cnt = pp.size();   //经过上一步之后,总类数减1,所以需要更新类计数器cnt的值


    #if 0


    M = cal_distance_array(pp);   //重新计算距离矩阵


    #else   //这里完全重新计算距离矩阵也是可以的;或者先删除被合并类所在的行列,然后仅重新计算合并类与其它类的距离也是OK的,理论上后者的效率会更高一点


    delete_row_col(M, jj);   //删除jj行与jj列
    for(int i = 0; i < cnt; i++)   //合并类的索引号仍为ii,更新在距离矩阵中合并类所在的ii行与ii列
    {
      if(ii == i)
      {
        M[ii][i] = 0;
      }
      else
      {
        float d = cal_cluster_distance(pp[ii], pp[i]);
        M[ii][i] = d;
        M[i][ii] = d;
      }
      
    }


    #endif
    
  }


  return pp;
}

主函数代码:

int main(void)
{
  float dat[][2]= {{1,1}, {6,5}, {1,1.5}, {11,9.5}, {1.5,1}, {6,5.5}, {10.5,10}, {1.5,1.5}, {1.5,2}
                  ,{6,6}, {10,9.5}, {6.5,5}, {10.5,9}, {6.5,5.5}, {2,1.5}, {6.5,6}, {11,10}, {7,6}
                  ,{10,9}, {7,5}, {10,10}, {2,2}, {10.5,9.5}, {7,5.5}, {2,1}, {11,9}, {1,2}};
  
  int len = sizeof(dat)/sizeof(dat[0]);
  vector<point> p;


  for(int i = 0; i < len; i++)   
  {
    point tmp;                  //新建一个类
    tmp.x = dat[i][0];    //将数据集写入类中
    tmp.y = dat[i][1]; 
    p.push_back(tmp);         //将类加入到点集中
  }
  
  vector<vector<point>> Clusters = Agnes(p, 3);   //设置期望的类总数为3
  


  printf("Clusters.size()= %d\n", Clusters.size());


  for(int i = 0; i < Clusters.size(); i++)   //打印分类结果
  {
    cout<<"第"<<i+1<<"簇包含的点:"<<endl;
    for(int j = 0; j < Clusters[i].size(); j++)
    {
      cout<<" ("<<Clusters[i][j].x<<","<<Clusters[i][j].y<<")";
    }
    cout<<endl;
  }
  
  return 0;
}

运行上述代码,得到结果如下:

AGNES算法相比于DBSCAN算法,其参数很少,只需要输入要分类的总数以及数据样本即可,但是AGNES算法比DBSCAN算法慢得多。

微信公众号如下,欢迎关注:

posted @ 2020-12-07 21:55  萌萌哒程序猴  阅读(856)  评论(0编辑  收藏  举报