Mengdong的技术博客

学习,记录,分享

导航

《集体智慧编程》第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))

为了计算方便,我们需要将不同文档对应的单词向量映射到统一的向量空间。整合单词向量的方法是合并所有文档的单词,构造统一的单词表,然后根据这个单词表构造特征向量。这样构造出来的特征向量就可以用于计算文章的相似度了。

例如对于D1D2,构造的字典和对应的单词计数为:

表3-1 D1D2的单词计数表

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”),D1D2对应的特征向量为V1’=(1,1,1,1,0,0)和V2’=(0,1,0,1,1,1).

有了形如V1’和V1’这样的特征向量,就可以对文档进行聚类了。

4 距离/相似度计算

有了特征向量这个标识文档特征的信息,我们还需要计算特征向量间距离/相似度的方法。

相似度S是一个数,两个文档越相似,S越大。而距离D恰好相反,两个文档越相似,D越小。

由于SD之间存在的这种负相关关系,我们可以通过一些简单的计算(例如倒数S=1/D和差S=1-D)实现SD之间的转换。

4.1 相似度的计算方法

4.1.1 Pearson相关系数

对于两个向量A(a1,a2...an)与B(b1,b2...bn),他们的Pearson相关系数可以计算如下:

SP位于[0,1]之间,SP为1代表AB具有最大相似度(AB完全相同),为0时表示AB完全不相关。DP=1-SP则可以用来表示AB之间的距离。显然AB越相似,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,p2pn)和Q(q1,q2qn),则两点的欧氏距定义为:

这个公式同样可以用于计算两个向量间的距离。

4.2.2 曼哈顿距

曼哈顿距定义的是“街区最短距离”,对于PQ,曼哈顿距离定义如下:

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的特征向量。例如类别CAB合并而来,AB的特征向量分别为(a1,a2...an)与(b1,b2...bn),则C的特征向量可以记作((a1+b1)/2, (a2+b2)/2... (an+bn)/2).

我们因此可以设计合并cluster类bicluster:

#合并类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代码):

#层次化聚类主函数,参数vectors为所有文档的特征向量,函数返回最后合并完的一个cluster
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的数组,其中的每个元素也为一个数组,存放的是当前类别中的向量。

posted on 2011-07-14 17:41  mdyang  阅读(3029)  评论(1编辑  收藏  举报