当前位置: 首页 > news >正文

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

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

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

相关文章:

  • 美国FPL太阳能中心安装完成50万块光伏板
  • 非常酷!10个基于 HTML5 的字体应用演示网站
  • MapView
  • Synaptics研发端到端加密的指纹传感器
  • 项目质量管理
  • MyEclipse 下用link 方式安装插件
  • [转载]c/c++ 操作sqlite
  • 应急制冷精密空调 数据中心应急制冷系统
  • Android高手的六大境界
  • 中企欲利用切尔诺贝利大片土地与充足阳光建光伏电站
  • 原生AJAX基础讲解及兼容处理
  • C++ stl
  • 如何修改多文章主窗口显示名
  • 公安部正制定网络安全保护条例 大数据保护机制将完善
  • 通过 C# 代码操作 Google 日历
  • 2017 前端面试准备 - 收藏集 - 掘金
  • 30秒的PHP代码片段(1)数组 - Array
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • flask接收请求并推入栈
  • JavaScript 基本功--面试宝典
  • Js基础知识(一) - 变量
  • Puppeteer:浏览器控制器
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 判断客户端类型,Android,iOS,PC
  • 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)
  • 学习JavaScript数据结构与算法 — 树
  • Semaphore
  • zabbix3.2监控linux磁盘IO
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • # 数论-逆元
  • #《AI中文版》V3 第 1 章 概述
  • #QT(智能家居界面-界面切换)
  • (1)(1.9) MSP (version 4.2)
  • (二)pulsar安装在独立的docker中,python测试
  • (翻译)Entity Framework技巧系列之七 - Tip 26 – 28
  • (附源码)spring boot北京冬奥会志愿者报名系统 毕业设计 150947
  • (附源码)计算机毕业设计高校学生选课系统
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (五)网络优化与超参数选择--九五小庞
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (转)fock函数详解
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • (转载)从 Java 代码到 Java 堆
  • .libPaths()设置包加载目录
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .net 微服务 服务保护 自动重试 Polly
  • .NET 中 GetProcess 相关方法的性能
  • .Net 中Partitioner static与dynamic的性能对比
  • .NET 中的轻量级线程安全
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .netcore 如何获取系统中所有session_ASP.NET Core如何解决分布式Session一致性问题
  • .NET大文件上传知识整理