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 |
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<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的值
M = cal_distance_array(pp); //重新计算距离矩阵
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;
}
}
}
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算法慢得多。