《集体智慧编程》第3章:浅谈文档聚类
1 前言
这篇读书笔记根据《集体智慧编程》第3章:聚类写成。本系列目录:http://www.cnblogs.com/mdyang/archive/2011/07/07/PCI-contents.html
本文先对监督学习和无监督学习的概念进行简要介绍,引出聚类。然后给出聚类的一个经典问题:文档聚类的描述,并介绍构造特征向量和计算向量之间距离/相似度的方法。在此基础上给出使用两种基本聚类算法(层次聚类、K均值聚类)解决文档聚类问题的解法。
2 监督学习与无监督学习
简单地说,监督学习就是需要输入正确样例进行预训练的学习。预训练可以理解为告诉程序“怎么做”的过程。监督学习,例如神经网络、决策树、支持向量机(SVM)、贝叶斯分类器等,都需要进行预学习。通过预学习,程序从正确样例中抽取规则,这些规则可用来对未来情况进行判定。
无监督学习则无需输入正确样例,即不告诉程序“怎么做”,而是让其自己决定。
本章提到的两种聚类算法即为无非监督学习算法。这些算法让程序自己发现哪些类适合聚为一类,而无需预先提供诸如“例如A,B,C三类可以聚为一类”这样的正确样例信息,对分类规则进行训练。
3 文档聚类:一个简单示例
3.1 问题描述
假定现在有很多文档需要按照特征聚类。每个文档为纯文本文件,为了描述方便起见,每个文档视为一个单词序列(字符串数组),单词都由小写字母构成(省略toLowerCase操作)。
3.2 构造特征向量
聚类是将类似的项目归到一起,既然类似,就需要有能够描述项目特征的信息。因此为了对这些文档进行聚类,我们首先需要描述文档特征。在这里我们采用单词向量作为描述文档的特征向量。单词向量VW = ((w1,c1), (w2,c2)… (wn,cn))是一个向量,表示单词wi在文档中出现了ci次(i∈[1,n])。例如对于文档:
D1: this is my phone,其对应的单词向量就是V1 = ((“this”,1), (“is”,1), (“my”,1), (“phone”,1))
D2: the phone is mine,对应V2 = ((“the”,1), (“phone”,1), (“is”,1), (“mine”,1))
为了计算方便,我们需要将不同文档对应的单词向量映射到统一的向量空间。整合单词向量的方法是合并所有文档的单词,构造统一的单词表,然后根据这个单词表构造特征向量。这样构造出来的特征向量就可以用于计算文章的相似度了。
例如对于D1和D2,构造的字典和对应的单词计数为:
表3-1 D1和D2的单词计数表
this |
is |
my |
phone |
the |
mine |
|
D1 |
1 |
1 |
1 |
1 |
0 |
0 |
D2 |
0 |
1 |
0 |
1 |
1 |
1 |
这样就可以构造单词表word_list = (“this”, “is”, “my”, “phone”, “the”, “mine”),D1和D2对应的特征向量为V1’=(1,1,1,1,0,0)和V2’=(0,1,0,1,1,1).
有了形如V1’和V1’这样的特征向量,就可以对文档进行聚类了。
4 距离/相似度计算
有了特征向量这个标识文档特征的信息,我们还需要计算特征向量间距离/相似度的方法。
相似度S是一个数,两个文档越相似,S越大。而距离D恰好相反,两个文档越相似,D越小。
由于S和D之间存在的这种负相关关系,我们可以通过一些简单的计算(例如倒数S=1/D和差S=1-D)实现S和D之间的转换。
4.1 相似度的计算方法
4.1.1 Pearson相关系数
对于两个向量A(a1,a2...an)与B(b1,b2...bn),他们的Pearson相关系数可以计算如下:
SP位于[0,1]之间,SP为1代表A与B具有最大相似度(A与B完全相同),为0时表示A与B完全不相关。DP=1-SP则可以用来表示A与B之间的距离。显然A与B越相似,DP越小。以下小节的聚类计算就基于文档之间的DP值进行。
4.1.2 Jacaard系数
Jacaard系数也称谷本系数,可用于计算集合(本质为二值向量)之间的相似度。集合A和B之间的Jacaard系数定义为:
这里的绝对值号指的是集合中的元素数量,例如集合(1,2,3)与集合(1,4,5)之间的Jacaard系数为|(1)|/|(1,2,3,4,5)|=1/5=0.2. 显然当两个集合相等时Jacaard系数取得最大值1,而两集合交集为空时Jacaard系数取得最小值0.
4.2 距离的计算方法
4.2.1 欧氏距
欧氏距是计算空间中两点直线距离的方法:给定两个点P(p1,p2 … pn)和Q(q1,q2 … qn),则两点的欧氏距定义为:
这个公式同样可以用于计算两个向量间的距离。
4.2.2 曼哈顿距
曼哈顿距定义的是“街区最短距离”,对于P和Q,曼哈顿距离定义如下:
4.2.3 马氏距
详见Wikipedia: http://en.wikipedia.org/wiki/Mahalanobis_distance
更多距离/相似度计算方法可见Wikipedia: http://en.wikipedia.org/wiki/Metric_(mathematics)#Examples
5 层次聚类
有了特征向量,也有了计算向量间距离/相似度的方法,我们就可以对文档进行聚类了。
层次聚类是最简单的一种聚类,它的基本操作步骤如下:
1 初始:每个文档单独为一个cluster 2 现在共有1个cluster?如果满足,算法结束。否则GOTO 3 3 计算每两个cluster之间的距离,并将距离最近的两个cluster合并为一个 4 GOTO 2 |
用文字描述层次聚类算法就是:每次合并最接近的两个cluster,直到不能合并(只剩一个cluster)为止,如下图所示。
需要计算两两cluster之间的距离,因此算法第一次迭代的复杂度为O(n2). 如果将这一步的结果缓存起来,则下一次迭代只需要计算n-2次距离(例如上图,只需要计算(A, B)与C、D、E之间的距离,其他的距离已保存),以此类推,从第二次迭代至最后聚类完毕,共需进行(n-2)+(n-3)+...+1=(n-1)(n-2)/2次距离计算,因此复杂度也为O(n2). 综合来看算法的复杂度为O(n2).
由于需要保存两两cluster之间的距离,因此空间复杂度为O(n2).
这个算法具有如下问题需要解决:
1. 两个cluster之间的距离如何计算?
2. 如何选取要合并的cluster?
3. 如何合并cluster?
问题1在第4节已经讨论过了,因此只剩下2和3需要解决。
对于2,可以保存当前最相似的两个cluster以及它们间的距离,在每次计算两个cluster之间的距离时,检查这个距离是否比保存的最短距离更短,如果是,则更新维护的最短距离变量的值,并将维护的两个最相似cluster更新为当前的两个cluster。
问题3的本质是如何用特征向量描述合并后的cluster。由于一个合并cluster是由两个子cluster合并得到的,因此可以对取这两个子cluster特征向量的平均值作为合并cluster的特征向量。例如类别C由A和B合并而来,A和B的特征向量分别为(a1,a2...an)与(b1,b2...bn),则C的特征向量可以记作((a1+b1)/2, (a2+b2)/2... (an+bn)/2).
我们因此可以设计合并cluster类bicluster:
class bicluster:
def __init__(self,vec,left=None,right=None,distance=0.0,id=None):
#合并类的一个子类
self.left=left
#合并类的另一个子类。当前类为原子类(即一个文档)时left和right都为None
self.right=right
#当前类的特征向量
self.vec=vec
#当前类的ID
self.id=id
#当前类两个子类的距离。当前类为原子类时distance为0.0
self.distance=distance
有了以上信息,就可以进行文档聚类了(Python代码):
def hcluster(vectors)
#距离缓存,distances[(i,j)](如果存在的话)存的是i,j之间的距离
distances = {}
#维护新合并cluster的ID的变量
currentclustid = -1
#构造初始cluster,每个文档为一个cluster
clust = [bicluster(vectors[i],id=i) for i in range(len(rows))]
while len(clust)>1:
#初始最接近类为0和1(即vectors[0]和vectors[1])
lowestpair=(0,1)
#初始最近距离为0和1之间的距离
closest=distance(clust[0].vec,clust[1].vec)
#二重循环
for i in range(len(clust)):
for j in range(i+1,len(clust)):
#如果i,j之间的距离还没算
if (clust[i].id,clust[j].id) not in distances:
#计算i,j之间的距离并存入distances[i][j],distance函数计算clust[i].vec和clust[j].vec两个向量间的距离
distances[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
d = distances[(clust[i].id,clust[j].id)]
#如果当前i,j之间的距离小于保存的最小距离
if d < closest:
#更新最近距离值和cluster对
closest=d
lowestpair=(i,j)
#二重循环结束
#计算合并cluster的特征向量(最接近cluster对的特征向量的平均向量)
mergevec=[
(clust[lowestpair[0]].vec[i]+clust[lowestpair[1]].vec[i])/2.0
for i in range(len(clust[0].vec))]
#构造合并cluster
newcluster = bicluster(mergevec, left=clust[lowestpair[0]], right=clust[lowestpair[1]], distance=closest, id=currentclustid)
#合并cluster的ID是-1,-2,-3...
currentclustid-=1
#删除已合并的cluster,并将合并cluster加入clust变量
del clust[lowestpair[1]]
del clust[lowestpair[0]]
clust.append(newcluster)
#while循环结束
#这时候clust里只剩一个cluster了,算法结束
return clust[0]
聚类将会生成具有类似下图的聚类结构,树中的每个节点为一个cluster,其中叶节点为文档,根节点为hcluster的返回值。
按列聚类
以单词为标度为文档建立特征向量,可以将类似的文档聚类(对表3-1中的行聚类)。同样,如果以文档为标度为单词建立特征向量,则可以将有关联的单词聚类(对表3-1中的列聚类)。聚类方法与按行聚类的方法相同,在此不再赘述。
6 K均值聚类
与层次聚类不同,K均值聚类的操作步骤如下:
1 根据向量集均值聚类首先计算向量空间的上、下界以确定一个区域R 2 在R内随机投放K个点(K即为需要的最终cluster数,即需要将文档聚为K类) 3 将每个向量与距离它最近的点P绑定 4 更新P的坐标为所有与P绑定的向量的平均值 5 不断迭代执行3-4,直到每个向量的绑定点都稳定下来不再变化(收敛) |
对于K均值聚类过程的一个形象描述可见下图(K=2):
如果输入向量的规模为n(即有n个文档要聚类),那么步骤1的时间复杂度为O(n),步骤2的时间复杂度为O(K),执行一次步骤3的时间复杂度为O(K×n). 若设置步骤3的最大迭代次数为M,则步骤3的总体复杂度为O(M×K×n). 执行一次步骤4的复杂度为O(n),步骤4的总体复杂度为O(M×n). 因此算法复杂度为O(M×K×n).
事实上,K均值聚类往往只需要很少的迭代步数就可以收敛,因此K均值聚类的效率大大高于层次聚类。
由于需要保存点与向量的绑定信息,因此空间复杂度为O(K×n).
下面直接给出K均值聚类的Python源码:
#K均值聚类主函数,默认聚为K类 def kcluster(vectors,k=4): #确定每一维的上、下界 ranges=[(min([row[i] for row in rows]),max([row[i] for row in rows])) for i in range(len(rows[0]))] #随机放置K个点 clusters=[[random.random()*(ranges[i][1]-ranges[i][0])+ranges[i][0] for i in range(len(rows[0]))] for j in range(k)] #放置上次的绑定方案 lastmatches=None #最多迭代100次 for t in range(100): bestmatches=[[] for i in range(k)] #查找对于每个向量,哪个点距离最近 for j in range(len(rows)): fow=fows[j] bestmatch=0 for i in range(k): d=distance(clusters[i],row) if d<distance(clusters[bestmatch],row): bestmatch=i #for i结束 bestmatches[bestmatch].append(j) #for j结束 #本次迭代完和上次结果一样,代表已收敛,结束循环 if bestmatches==lastmatches: break #上次结果更新为本次结果 lastmatches=bestmatches #遍历K个点,并更新坐标 for i in range(k): #avgs存放平均值 avgs=[0.0]*len(rows[0]) #当前点有向量与之绑定 if len(bestmatches[i])>0: #加和 for rowid in bestmatches[i]: for m in range(len(rows[rowid])): avgs[m]+=rows[rowid][m] #求平均 for j in range(len(avgs)): avgs[j]/=len(bestmatches[i]) #更新坐标 clusters[i]=avgs #for i结束 #for t结束 return bestmatches
算法返回的bestmatches为长度为K的数组,其中的每个元素也为一个数组,存放的是当前类别中的向量。