代码改变世界

全文检索、数据挖掘、推荐引擎系列5---文章术语向量表示法

2011-08-19 16:39  java ee spring  阅读(286)  评论(0编辑  收藏  举报

无论是要进行全文检索,还是对文章进行自动聚类分析,都需要将文章表示为术语向量(Term Vector),在Lucene内部就是通过术语向量来对文章进行索引和搜索的,但是Lucene没有向外提供合适的术语向量计算接口,所以对术语向量计算还必须我们自己来做。

术语向量解述

众所周知,一篇文章由一个个的单词组成,我们在进行文本处理时,首先进行中文分词,包括去除“的、地、得”等常用停止词,对关键词加上同义词,如缩写和全称,如果是英文可能还需要变为小写,去除复数和过去分词等,可能还需要提取词根,总之经过上述步聚的预处理,文章将变成由一系列单词组成的字符串数组。

对一系统中的每一篇文章,我们首先计算每个单词的出现频率(TF:TermFrequency),即该单词出现的次数除以文章总单词数,然后统计这个单词的反比文档频率(IDF:Inverse Document Frequency),在所有文章中出现的次数,并用该数除文章总数,即总文章数除以出现该单词文章的数目。由上面的定义可以看出,单词越重要,他的单词出现频率TF就越高,单词越是只在这篇文章中出现,很少在其它文章中出现,那该单词越对本篇文章具有重要意义。通过一定的公式,可以计算出每个单词的对每篇文章的权重,这样所有单词加上其对应的权重,就形成了一个多维术语向量。

计算TF*IDF

对于术语向量的计算方法,目前还没有特别成熟的算法,现在常用的只是一些经验算法,一些文章中提出的号称更加准确的算法,还没有经过实际验证。我们在这里采用的是当前最常用的算法,根据实际需要对这些算法进行修正也是相当简单的。

首先系统需要维护几个全局变量:总文章数、系统中所有单词以及其在文章中出现的次数


 // 因为单词出现在文章的不同位置重要性不同,可以设置不同的权重
 public final static int TITLE_WEIGHT = 1;
 public final static int KEYWORD_WEIGHT = 1;
 public final static int TAG_WEIGHT = 1;
 public final static int ABCT_WEIGHT = 1;
 public final static int BODY_WEIGHT = 1;
 
 private static int docsNum = 0; // 目前系统中的总文档数,将来需要存在数据库中
 private static Map<String, Integer> wordDocs = null; // 每个单词在所有的每篇文章中出现的文章个数(文章数)
 private static Vector<Integer> termNumDoc = null; // 每篇文章的总单词数
 private static Vector<Vector<TermInfo>> termVectors = null; // 每篇文章的术语向量表示

 

然后是对一段文本产生术语原始术语向量的程序,如下所示:
 /**
  * 一篇文章分为标题、关键字、摘要、标志、正文几个部分组成,每个部分的单词具有不同的权重,通过
  * 本函数进行中文分词,同时生成该部分的术语向量
  * @param text 需要处理的文本
  * @param termArray 术语向量
  * @param weight 单词在本部分的权重
  * @return 本部分的单总数(用于计算单词出现频率TF)
  */
 private static int procDocPart(String text, Vector<TermInfo> termArray, int weight) {
  Collection<String> words = FteEngine.tokenize(text);
  Iterator<String> itr = words.iterator();
  String word = null;
  TermInfo termInfo = null;
  int termMount = 0;
  while (itr.hasNext()) {
   word = itr.next();
   if (termArray.contains(word)) {
    termInfo = termArray.get(termArray.indexOf(word));
    termInfo.setMountPerDoc(termInfo.getMountPerDoc() + weight);
   } else {
    termInfo = new TermInfo();
    termInfo.setMountPerDoc(weight);
    termInfo.setTermStr(word);
    termInfo.setRawWeight(0.0);
    termInfo.setWeight(0.0);
    termArray.add(termInfo);
   }
   termMount += weight;
  }
  return termMount;
 }

下面是求出TF*IDF然后进行归一化生成最终术语向量的程序:

/**
  * 对标题、关键字、标记、摘要、正文采用迭加方式生成术语向量
  * @param docIdx 文档编号,为-1时表示新加入的文档
  * @param text 需要处理的文本
  * @param weight 本段文本单词出现的权重
  * @return 文档编号
  */
 public static int genTermVector(int docIdx, String text, int weight) {
  Vector<TermInfo> termVector = null;
  if (docIdx < 0) {
   docIdx = docsNum;
   termNumDoc.add(0);
   termVector = new Vector<TermInfo>();
   termVectors.add(termVector);
   docsNum++;
  }
  termVector = termVectors.elementAt(docIdx);
  int termMount = procDocPart(text, termVector, weight);
  termNumDoc.set(docIdx, termNumDoc.elementAt(docIdx).intValue() + termMount);
  // 计算所有术语的IDF
  TermInfo termInfo = null;
  String termStr = null;
  Iterator<TermInfo> termInfoItr = termVector.iterator();
  // 计算每个单词在文章中出现的篇数
  while (termInfoItr.hasNext()) {
   termInfo = termInfoItr.next();
   termStr = termInfo.getTermStr();
   if (wordDocs.get(termStr) != null) {
    wordDocs.put(termStr, wordDocs.get(termStr).intValue() + 1);
   } else {
    wordDocs.put(termStr, 1);
   }
   termInfo.setTf(termInfo.getMountPerDoc() / ((double)termNumDoc.elementAt(docIdx).intValue()));
  }
  Iterator<Vector<TermInfo>> docItr = termVectors.iterator();
  // 计算TF*IDF
  double rwPSum = 0.0;
  while (docItr.hasNext()) {
   termVector = docItr.next();
   termInfoItr = termVector.iterator();
   rwPSum = 0.0;
   while (termInfoItr.hasNext()) {
    termInfo = termInfoItr.next();
    termInfo.setRawWeight(termInfo.getTf() * Math.log(((double)docsNum) /
      wordDocs.get(termInfo.getTermStr()).intValue()));
    rwPSum += termInfo.getRawWeight() * termInfo.getRawWeight();
   }
   // 对TF*IDF进行归一化
   termInfoItr = termVector.iterator();
   while (termInfoItr.hasNext()) {
    termInfo = termInfoItr.next();
    termInfo.setWeight(termInfo.getRawWeight() / Math.sqrt(rwPSum));
   }
  }
  return docIdx;
 }

 

文章相似度计算

文章的相似度就是要计处两篇文章对应的术语向量的距离,也就是对应各个术语归一化后的TF*IDF的权重差的平方合再开发,类似于二维矢量距离的计算,具体实现代码如下所示:

/**
  * 计算术语向量的距离,该值小则表明两篇文章相似度高
  * @param termVector1
  * @param termVector2
  * @return 距离
  */
 public static double calTermVectorDist(Collection<TermInfo> termVector1, Collection<TermInfo> termVector2) {
  double dist = 0.0;
  Vector<TermInfo> tv1 = (Vector<TermInfo>)termVector1;
  Vector<TermInfo> tv2 = (Vector<TermInfo>)termVector2;
  Hashtable<String, TermInfo> tv2Tbl = new Hashtable<String, TermInfo>();
  Iterator<TermInfo> tvItr = null;
  TermInfo termInfo = null;
  TermInfo ti2 = null;
  double[] weights = new double [tv2.size()];
  int idx = 0;
  // 初始化数据
  tvItr = tv2.iterator();
  while (tvItr.hasNext()) {
   termInfo = tvItr.next();
   //weights[idx++] = termInfo.getWeight();
   tv2Tbl.put(termInfo.getTermStr(), termInfo);
  }
  //
  tvItr = tv1.iterator();
  while (tvItr.hasNext()) {
   termInfo = tvItr.next();
   ti2 = tv2Tbl.get(termInfo.getTermStr());
   if (ti2 != null) {
    dist += (termInfo.getWeight() - ti2.getWeight()) * (termInfo.getWeight() - ti2.getWeight());
    ti2.setWeight(0.0);
   } else {
    dist += termInfo.getWeight() * termInfo.getWeight();
   }
  }
  tvItr = tv2Tbl.values().iterator();
  while (tvItr.hasNext()) {
   termInfo = tvItr.next();
   System.out.println("######: " + termInfo.getTermStr() + "=" + termInfo.getWeight() + "!");
   dist += termInfo.getWeight() * termInfo.getWeight();
  }
  System.out.println();
  
  return Math.sqrt(dist);
 }

下面对以下三句话进行计算:

Java语言编程技术详解

C++语言编程指南

同性恋网站变身电子商务网站

计算的术语向量值为:

java:0.5527962688403749
语言:0.20402065516569604
编程:0.20402065516569604
技术:0.5527962688403749
详解:0.5527962688403749
############## doc2 ############
c:0.6633689723434504           (注:我们的词典中没有C++)
语言:0.24482975009584626
编程:0.24482975009584626
指南:0.6633689723434504
############## doc3 ############
同性恋:0.531130184813292
网:0.196024348194679
站:0.196024348194679
变身:0.531130184813292
电子商务:0.531130184813292
网:0.196024348194679
站:0.196024348194679

然后计算距离为:

第一篇与第二篇:1.3417148340558687

第一篇与第三篇:1.3867764455130116

因此通过计算结果系统会认为第一篇和第二篇更接近,实际情况也是如此,因为第一篇和第二篇间有两个单词是相同的,而第一篇和第三篇间则没有任何相同的地方。