与前文介绍的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
d12
d13
d14
d15
C
d20
d21
d22
d23
d24
d25
D
d30
d31
d32
d33
d34
d35
E
d40
d41
d42
d43
d44
d45
F
d50
d51
d52
d53
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
d12
d13
d14
D
d20
d21
d22
d23
d24
E
d30
d31
d32
d33
d34
F
d40
d41
d42
d43
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
d12
d13
DE
d20
d21
d22
d23
F
d30
d31
d32
d33
上表中,假设矩阵中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 p1, vector 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> cal_distance_array(vector> p){vector> d;for(int i = 0; i < p.size(); i++) //计算类集合中所有簇的两两之间的簇距离,组成距离矩阵{vector 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> 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> &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> &p, int A_idx, int B_idx){p[A_idx].insert(p[A_idx].end(), p[B_idx].begin(), p[B_idx].end()); //将簇B合并到簇Ap.erase(p.begin()+B_idx); //删除类B}
下面上正餐,AGNES算法计算实现:
vector> Agnes(vector p, int cluster_num){vector> pp;for(int i = 0; i tmp;tmp.push_back(p[i]);pp.push_back(tmp);}vector> 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 0M = cal_distance_array(pp); //重新计算距离矩阵#else //这里完全重新计算距离矩阵也是可以的;或者先删除被合并类所在的行列,然后仅重新计算合并类与其它类的距离也是OK的,理论上后者的效率会更高一点delete_row_col(M, jj); //删除jj行与jj列for(int i = 0; i
主函数代码:
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 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> Clusters = Agnes(p, 3); //设置期望的类总数为3printf("Clusters.size()= %d\n", Clusters.size());for(int i = 0; i < Clusters.size(); i++) //打印分类结果{cout<
运行上述代码,得到结果如下:
AGNES算法相比于DBSCAN算法,其参数很少,只需要输入要分类的总数以及数据样本即可,但是AGNES算法比DBSCAN算法慢得多。