您的位置:首页 > 其它

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

2011-08-19 16:39 351 查看
无论是要进行全文检索,还是对文章进行自动聚类分析,都需要将文章表示为术语向量(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

因此通过计算结果系统会认为第一篇和第二篇更接近,实际情况也是如此,因为第一篇和第二篇间有两个单词是相同的,而第一篇和第三篇间则没有任何相同的地方。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐