基于Java的文本相似度计算 |
您所在的位置:网站首页 › 文本相似度算法对比分析 › 基于Java的文本相似度计算 |
目录
1. 前言1.1 开发环境:1.2 初步设想1.3 参考资料
2. HanLP2.1 在Java中使用HanLP库2.2 分词函数
3. 双文本对比3.1 步骤分解3.2 完整代码
1. 前言
最近在做一个基于SSM的Web项目,其中有一项功能是 对相似文本进行合并 ,其中涉及一个文本间相似度计算的问题。在此将实现过程记录下来。 1.1 开发环境: 名称版本操作系统Win10 X64JDK1.8.0_144InteIliJ IDEA2020.1Tomcat9.0.29 1.2 初步设想开始前有三个技术问题待解决 : 第一个问题是如何衡量文本的相似性。拟采用余弦相似性的方法。余弦相似度的计算单元是向量,因此第二个问题是如何将文本转换为向量。第三个问题是如何将一对一比较转换为多对多比较,按相似度将语料库中的多个文本进行相似度聚类。(这个问题并没有得到解决) 1.3 参考资料 CSDN博客 – Java 实现计算文本相似度 (使用余弦定理) : 本文许多代码是参考这篇博客里的。这篇博客的内容感觉有好多篇与它类似,我也不知道找的是不是原版…CSDN博客 – JAVA-简单实现文本相似度计算-余弦相似度 : 这篇博文提供了一个不使用 HanLP 库进行分词的简单案例,是基于字进行分割的。 2. HanLPHan Language Processing 是一个自然语言处理工具包,基于PyTorch和TensorFlow 2.x双引擎实现。可以在多种语言环境下引入HanLP包,利用其中封装好的API进行快捷的NLP开发。 Github仓库地址 :https://github.com/hankcs/HanLP官方文档地址 : https://hanlp.hankcs.com/docs/ 2.1 在Java中使用HanLP库我构建的是一个 Maven 项目,因此只需要在项目的pom.xml文件中引入 hanlp 依赖即可。 com.hankcs hanlp portable-1.8.1但是我在引入包后运行程序,仍然出现了 Error:(3, 24) java: 程序包com.hankcs.hanlp不存在 ,解决方法是在 File -> Settings -> Build,Execution,Deployment -> Build Tools -> Maven -> Runner 中勾选 Delegate IDE build/run actions to Maven。 但这样处理之后似乎有一个弊端,就是 Maven 的 Build 进程会循环运行,拖慢整个 Web 应用(甚至是整台电脑)的反应时间。 后来这个方法对电脑运行的拖累实在是太大了,于是笔者不得已找了另一个方法。系统提示找不到包,那么直接包缺少的包下载下来引入就行了。步骤如下: 前往下载 hanlp 的 jar 包,网址:https://github.com/hankcs/HanLP/releases![]() ![]() ![]() 利用 HanLP 中的 segment 函数进行中文分词,代码如下。 import com.hankcs.hanlp.HanLP; import com.hankcs.hanlp.seg.common.Term; public void test01(){ String text = "我在吉林大学软件学院学习计算机。今天是阳光明媚的一天"; List words= HanLP.segment(text); for (Term word : words) { System.out.print(word.word + ","); } }得到结果如下: Nature 是一个枚举类,其取值范围很广,以下罗列一些较为常见的词型: 取值含义说明r代词r 指代词,r* 可代表不同类的代词。如:rr 是人称代词,ry 是疑问代词 etc.v动词v 指动词,v* 可代表不同类的动词。如:vd 是副动词,vn 是名动词 etc.ns地名如:吉林、长春u*助词uj、ud 指助词,ul、uv 指连词 etc.m数词m 指数词q量词q 指量词n名词n 指名词,n* 代表具体名词种类。如:nr 是人名,nrf 是音译人名w标点符号w* 代表具体的标点符号。如:wkz 代表左括号,ww 代表问号d副词如:真、太p介词p 代表介词,特殊的:pba – 把;pbei – 被c连词如:而且、但是a形容词a* 代表具体的形容词种类。如:ad 是副形词;an 是名形词y语气词如:啊,诶 3. 双文本对比 3.1 步骤分解假设有两个文本 A 和 B,计算它们的相似程度。不妨建立一个工具类来做这个工作。 public class MyTextComparator{}第一步是将一个文本转换为一组词序列,这些词应该是有实际意义的。这里,我取了名词、动词、形容词、动名词四种词进行保留。 // 提取文本中有实意的词 public static List extractWordFromText(String text){ // resultList 用于保存提取后的结果 List resultList = new ArrayList(); // 当 text 为空字符串时,使用分词函数会报错,所以需要提前处理这种情况 if(text.length() == 0){ return resultList; } // 分词 List termList = HanLP.segment(text); // 提取所有的 1.名词/n ; 2.动词/v ; 3.形容词/a ; 4.动名词/vn for (Term term : termList) { if(term.nature == Nature.n || term.nature == Nature.v || term.nature == Nature.a || term.nature == Nature.vn){ resultList.add(term.word); } } return resultList; }得到结果如下: 看到这个结果,自然而然想到的一个问题是:究竟什么样的词更应该被保留下来?比如:第二句话的“认为”一词虽然是动词,但似乎没有必要保留;第一句话的“吉林大学”是不是更应该作为词组保留下来?第二个问题是:保留词的选择是否和文本领域有关?又应该有一个怎样的标准呢? 将单词数组转换为单词向量。 这里要说的一个概念是“词汇表”,词汇表由所有文本中出现的单词组成。另一个概念是“频数表”,频数表本质上是一个字典,key值为单词本身,value值为该词在整个文本中出现的次数。每一个文本都对应一个属于自己的频数表。 (1)将单词数组转换为单词向量的第一步就是构建词汇表和每个文本的频数表:逐个遍历文本,建立各自的频数表;同时,在建立频数表的同时,将新出现的单词加入词汇表。 (2)将第(1)步得到的频数表转换为频率表。假设频数表 A 的频数总和为 sum ,则其对应的频率表就是将 A 中各个元素(频数)除以 sum 得到频率。 (3)假设在第(1)步得到的词汇表中有 n 个词,那么我们最后要生成的单词向量也是 n 维的。每个文本根据第(2)步得到的频率表来构建单词向量。假设词汇表为{a1,a2,…an},频率表 A 中记录着以下统计量{a2:1/2,an:1/2},那么文本 A 对应的单词向量就是 (0,1/2,0,…0,1/2) (1)根据单词数组建立频率表和词汇表 /** * @param wordList:单词数组 * @param vocabulary: 词汇表 * @return Map: key为单词,value为频率 * @Description 建立词汇表 wordList 的频率表,并同时建立词汇表 */ public static Map buildFrequencyTable(List wordList,List vocabulary){ // 先建立频数表 Map countTable = new HashMap(); for (String word : wordList) { if(countTable.containsKey(word)){ countTable.put(word,countTable.get(word)+1); } else{ countTable.put(word,1); } // 词汇表中是无重复元素的,所以只在 vocabulary 中没有该元素时才加入 if(!vocabulary.contains(word)){ vocabulary.add(word); } } // totalCount 用于记录词出现的总次数 int totalCount = wordList.size(); // 将频数表转换为频率表 Map frequencyTable = new HashMap(); for (String key : countTable.keySet()) { frequencyTable.put(key,(double)countTable.get(key)/totalCount); } return frequencyTable; }(2)根据频率表得到词向量 /** * @param frequencyTable : 频率表 * @param wordVector : 转换后的词向量 * @param vocabulary : 词汇表 * @Description 根据词汇表和文本的频率表计算词向量,最后 wordVector 和 vocabulary 应该是同维的 */ public static void getWordVectorFromFrequencyTable(Map frequencyTable,List wordVector,List vocabulary){ for (String word : vocabulary) { double value = 0.0; if(frequencyTable.containsKey(word)){ value = frequencyTable.get(word); } wordVector.add(value); } }(3)综合 (1) (2) 实现将单词数组转换为词向量 /** * @Description : 将单词数组转换为单词向量,结果保存在 vectorA 和 vectorB 里 * @param wordListA : 文本 A 的单词数组 * @param wordListB : 文本 B 的单词数组 * @param vectorA : 文本 A 转换成为的向量 A * @param vectorB : 文本 B 转换成为的向量 B * @return vocabulary : 词汇表 */ public static List convertWordList2Vector(List wordListA,List wordListB,List vectorA,List vectorB){ // 词汇表 List vocabulary = new ArrayList(); // 获取词汇表 wordListA 的频率表,并同时建立词汇表 Map frequencyTableA = buildFrequencyTable(wordListA, vocabulary); // 获取词汇表 wordListB 的频率表,并同时建立词汇表 Map frequencyTableB = buildFrequencyTable(wordListB, vocabulary); // 根据频率表得到向量 getWordVectorFromFrequencyTable(frequencyTableA,vectorA,vocabulary); getWordVectorFromFrequencyTable(frequencyTableB,vectorB,vocabulary); return vocabulary; }(4) 简单测试一下函数(3)的效果 基于向量余弦值计算相似度 (2) 计算两个向量夹角的余弦值 /** * @Description 计算向量 A 和向量 B 的夹角余弦值 * @param vectorA : 词向量 A * @param vectorB : 词向量 B * @return */ public static double countCosine(List vectorA,List vectorB){ // 分别计算向量的平方和 double sqrtA = countSquareSum(vectorA); double sqrtB = countSquareSum(vectorB); // 计算向量的点积 double dotProductResult = 0.0; for(int i = 0;i // 两两对比函数 public static Double getCosineSimilarity(String textA,String textB){ // 从文本中提取出关键词数组 List wordListA = MyTextComparator.extractWordFromText(textA); List wordListB = MyTextComparator.extractWordFromText(textB); List vectorA = new ArrayList(); List vectorB = new ArrayList(); // 将关键词数组转换为词向量并保存在 vectorA 和 vectorB 中 MyTextComparator.convertWordList2Vector(wordListA,wordListB,vectorA,vectorB); // 计算向量夹角的余弦值 double cosine = Double.parseDouble(String.format("%.4f",MyTextComparator.countCosine(vectorA,vectorB))); return cosine; } // 提取文本中有实意的词 private static List extractWordFromText(String text){ // resultList 用于保存提取后的结果 List resultList = new ArrayList(); // 当 text 为空字符串时,使用分词函数会报错,所以需要提前处理这种情况 if(text.length() == 0){ return resultList; } // 分词 List termList = HanLP.segment(text); // 提取所有的 1.名词/n ; 2.动词/v ; 3.形容词/a for (Term term : termList) { if(term.nature == Nature.n || term.nature == Nature.v || term.nature == Nature.a || term.nature == Nature.vn){ resultList.add(term.word); } } return resultList; } /** * @Description : 将单词数组转换为单词向量,结果保存在 vectorA 和 vectorB 里 * @param wordListA : 文本 A 的单词数组 * @param wordListB : 文本 B 的单词数组 * @param vectorA : 文本 A 转换成为的向量 A * @param vectorB : 文本 B 转换成为的向量 B * @return vocabulary : 词汇表 */ private static List convertWordList2Vector(List wordListA,List wordListB,List vectorA,List vectorB){ // 词汇表 List vocabulary = new ArrayList(); // 获取词汇表 wordListA 的频率表,并同时建立词汇表 Map frequencyTableA = buildFrequencyTable(wordListA, vocabulary); // 获取词汇表 wordListB 的频率表,并同时建立词汇表 Map frequencyTableB = buildFrequencyTable(wordListB, vocabulary); // 根据频率表得到向量 getWordVectorFromFrequencyTable(frequencyTableA,vectorA,vocabulary); getWordVectorFromFrequencyTable(frequencyTableB,vectorB,vocabulary); return vocabulary; } /** * @param wordList:单词数组 * @param vocabulary: 词汇表 * @return Map: key为单词,value为频率 * @Description 建立词汇表 wordList 的频率表,并同时建立词汇表 */ private static Map buildFrequencyTable(List wordList,List vocabulary){ // 先建立频数表 Map countTable = new HashMap(); for (String word : wordList) { if(countTable.containsKey(word)){ countTable.put(word,countTable.get(word)+1); } else{ countTable.put(word,1); } // 词汇表中是无重复元素的,所以只在 vocabulary 中没有该元素时才加入 if(!vocabulary.contains(word)){ vocabulary.add(word); } } // totalCount 用于记录词出现的总次数 int totalCount = wordList.size(); // 将频数表转换为频率表 Map frequencyTable = new HashMap(); for (String key : countTable.keySet()) { frequencyTable.put(key,(double)countTable.get(key)/totalCount); } return frequencyTable; } /** * @param frequencyTable : 频率表 * @param wordVector : 转换后的词向量 * @param vocabulary : 词汇表 * @Description 根据词汇表和文本的频率表计算词向量,最后 wordVector 和 vocabulary 应该是同维的 */ private static void getWordVectorFromFrequencyTable(Map frequencyTable,List wordVector,List vocabulary){ for (String word : vocabulary) { double value = 0.0; if(frequencyTable.containsKey(word)){ value = frequencyTable.get(word); } wordVector.add(value); } } /** * @Description 计算向量 A 和向量 B 的夹角余弦值 * @param vectorA : 词向量 A * @param vectorB : 词向量 B * @return */ private static double countCosine(List vectorA,List vectorB){ // 分别计算向量的平方和 double sqrtA = countSquareSum(vectorA); double sqrtB = countSquareSum(vectorB); // 计算向量的点积 double dotProductResult = 0.0; for(int i = 0;i double result = 0.0; for (Double value : vector) { result += value*value; } return Math.sqrt(result); } }对 MyTextComparator 进行测试: |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |