目录

  • 1. 前言
    • 1.1 开发环境:
    • 1.2 初步设想
    • 1.3 参考资料
  • 2. HanLP
    • 2.1 在Java中使用HanLP库
    • 2.2 分词函数
  • 3. 双文本对比
    • 3.1 步骤分解
    • 3.2 完整代码

1. 前言

最近在做一个基于SSM的Web项目,其中有一项功能是 对相似文本进行合并 ,其中涉及一个文本间相似度计算的问题。在此将实现过程记录下来。

1.1 开发环境:

名称 版本
操作系统 Win10 X64
JDK 1.8.0_144
InteIliJ IDEA 2020.1
Tomcat 9.0.29

1.2 初步设想

开始前有三个技术问题待解决 :

  1. 第一个问题是如何衡量文本的相似性。拟采用余弦相似性的方法。
  2. 余弦相似度的计算单元是向量,因此第二个问题是如何将文本转换为向量。
  3. 第三个问题是如何将一对一比较转换为多对多比较,按相似度将语料库中的多个文本进行相似度聚类。(这个问题并没有得到解决)

1.3 参考资料

  1. CSDN博客 – Java 实现计算文本相似度 (使用余弦定理) : 本文许多代码是参考这篇博客里的。这篇博客的内容感觉有好多篇与它类似,我也不知道找的是不是原版…
  2. CSDN博客 – JAVA-简单实现文本相似度计算-余弦相似度 : 这篇博文提供了一个不使用 HanLP 库进行分词的简单案例,是基于字进行分割的。

2. HanLP

Han 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 依赖即可。

<dependency><groupId>com.hankcs</groupId><artifactId>hanlp</artifactId><version>portable-1.8.1</version>
</dependency>

但是我在引入包后运行程序,仍然出现了 Error:(3, 24) java: 程序包com.hankcs.hanlp不存在 ,解决方法是在 File -> Settings -> Build,Execution,Deployment -> Build Tools -> Maven -> Runner 中勾选 Delegate IDE build/run actions to Maven。

参考解答:https://zhuanlan.zhihu.com/p/142583125

但这样处理之后似乎有一个弊端,就是 Maven 的 Build 进程会循环运行,拖慢整个 Web 应用(甚至是整台电脑)的反应时间。

后来这个方法对电脑运行的拖累实在是太大了,于是笔者不得已找了另一个方法。系统提示找不到包,那么直接包缺少的包下载下来引入就行了。步骤如下:

  1. 前往下载 hanlp 的 jar 包,网址:https://github.com/hankcs/HanLP/releases
  2. 将下载好的 jar 包放入项目的 lib 文件夹下(这个 lib 文件夹是笔者自己建的),直接将 jar 包从下载好的压缩包解压得到的文件夹中拖入 IDEA 中即可。
  3. 通过右键项目 Open Module Settings,或者 File -> Project Structure 打开 1 所示窗口,按如下步骤进行操作,将 lib 中的 jar 包导入项目。

    参考解答:IDEA-idea中解决Java程序包不存在问题

2.2 分词函数

利用 HanLP 中的 segment 函数进行中文分词,代码如下。

import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.common.Term;
public void test01(){String text = "我在吉林大学软件学院学习计算机。今天是阳光明媚的一天";List<Term> words= HanLP.segment(text);for (Term word : words) {System.out.print(word.word + ",");}
}

得到结果如下:

  • HanLP 的 segment 函数具有中文分词功能,可以将作为参数传入的文本分成独立单词。
  • Term 代表一个单词,用户可以直接访问此单词的全部属性。单词包括 word(词语)Nature(词性)offset(文中起始位置)三个属性,均为 public 类型,可直接访问。

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 步骤分解

  1. 假设有两个文本 A 和 B,计算它们的相似程度。不妨建立一个工具类来做这个工作。

    public class MyTextComparator{}
    
  2. 第一步是将一个文本转换为一组词序列,这些词应该是有实际意义的。这里,我取了名词、动词、形容词、动名词四种词进行保留。

    // 提取文本中有实意的词
    public static List<String> extractWordFromText(String text){// resultList 用于保存提取后的结果List<String> resultList = new ArrayList<>();// 当 text 为空字符串时,使用分词函数会报错,所以需要提前处理这种情况if(text.length() == 0){return resultList;}// 分词List<Term> termList = HanLP.segment(text);// 提取所有的 1.名词/n ; 2.动词/v ; 3.形容词/a ; 4.动名词/vnfor (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;
    }
    

    得到结果如下:

    看到这个结果,自然而然想到的一个问题是:究竟什么样的词更应该被保留下来?比如:第二句话的“认为”一词虽然是动词,但似乎没有必要保留;第一句话的“吉林大学”是不是更应该作为词组保留下来?第二个问题是:保留词的选择是否和文本领域有关?又应该有一个怎样的标准呢?

  3. 将单词数组转换为单词向量。

    这里要说的一个概念是“词汇表”,词汇表由所有文本中出现的单词组成。另一个概念是“频数表”,频数表本质上是一个字典,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<String,Double>: key为单词,value为频率* @Description 建立词汇表 wordList 的频率表,并同时建立词汇表*/
    public static Map<String,Double> buildFrequencyTable(List<String> wordList,List<String> vocabulary){// 先建立频数表Map<String,Integer> 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<String,Double> 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<String,Double> frequencyTable,List<Double> wordVector,List<String> 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<String> convertWordList2Vector(List<String> wordListA,List<String> wordListB,List<Double> vectorA,List<Double> vectorB){// 词汇表List<String> vocabulary = new ArrayList<>();// 获取词汇表 wordListA 的频率表,并同时建立词汇表Map<String,Double> frequencyTableA = buildFrequencyTable(wordListA, vocabulary);// 获取词汇表 wordListB 的频率表,并同时建立词汇表Map<String,Double> frequencyTableB = buildFrequencyTable(wordListB, vocabulary);// 根据频率表得到向量getWordVectorFromFrequencyTable(frequencyTableA,vectorA,vocabulary);getWordVectorFromFrequencyTable(frequencyTableB,vectorB,vocabulary);return vocabulary;
    }
    

    (4) 简单测试一下函数(3)的效果

  4. 基于向量余弦值计算相似度

    (1)计算向量平方和开方的函数

    // 计算向量平方和的开方
    public static double countSquareSum(List<Double> vector){double result = 0.0;for (Double value : vector) {result += value*value;}return Math.sqrt(result);
    }
    

    (2) 计算两个向量夹角的余弦值

    /*** @Description 计算向量 A 和向量 B 的夹角余弦值* @param vectorA   : 词向量 A* @param vectorB   : 词向量 B* @return*/
    public static double countCosine(List<Double> vectorA,List<Double> vectorB){// 分别计算向量的平方和double sqrtA = countSquareSum(vectorA);double sqrtB = countSquareSum(vectorB);// 计算向量的点积double dotProductResult = 0.0;for(int i = 0;i < vectorA.size();i++){dotProductResult += vectorA.get(i) * vectorB.get(i);}return dotProductResult/(sqrtA*sqrtB);
    }
    

3.2 完整代码

因为在3.1节需要将类中许多函数分别测试,所以就把它们都设置成 public 权限类型的了,在完整代码把它们都改回来了,只留下了一个 public 类型的函数供用户直接传入文本得到文本相似性。

package com.test;import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.corpus.tag.Nature;
import com.hankcs.hanlp.seg.common.Term;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author Llunch4w* @create 2021-04-02 17:52*/
public class MyTextComparator {// 两两对比函数public static Double getCosineSimilarity(String textA,String textB){// 从文本中提取出关键词数组List<String> wordListA = MyTextComparator.extractWordFromText(textA);List<String> wordListB = MyTextComparator.extractWordFromText(textB);List<Double> vectorA = new ArrayList<>();List<Double> 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<String> extractWordFromText(String text){// resultList 用于保存提取后的结果List<String> resultList = new ArrayList<>();// 当 text 为空字符串时,使用分词函数会报错,所以需要提前处理这种情况if(text.length() == 0){return resultList;}// 分词List<Term> termList = HanLP.segment(text);// 提取所有的 1.名词/n ; 2.动词/v ; 3.形容词/afor (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<String> convertWordList2Vector(List<String> wordListA,List<String> wordListB,List<Double> vectorA,List<Double> vectorB){// 词汇表List<String> vocabulary = new ArrayList<>();// 获取词汇表 wordListA 的频率表,并同时建立词汇表Map<String,Double> frequencyTableA = buildFrequencyTable(wordListA, vocabulary);// 获取词汇表 wordListB 的频率表,并同时建立词汇表Map<String,Double> frequencyTableB = buildFrequencyTable(wordListB, vocabulary);// 根据频率表得到向量getWordVectorFromFrequencyTable(frequencyTableA,vectorA,vocabulary);getWordVectorFromFrequencyTable(frequencyTableB,vectorB,vocabulary);return vocabulary;}/*** @param wordList:单词数组* @param vocabulary: 词汇表* @return Map<String,Double>: key为单词,value为频率* @Description 建立词汇表 wordList 的频率表,并同时建立词汇表*/private static Map<String,Double> buildFrequencyTable(List<String> wordList,List<String> vocabulary){// 先建立频数表Map<String,Integer> 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<String,Double> 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<String,Double> frequencyTable,List<Double> wordVector,List<String> 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<Double> vectorA,List<Double> vectorB){// 分别计算向量的平方和double sqrtA = countSquareSum(vectorA);double sqrtB = countSquareSum(vectorB);// 计算向量的点积double dotProductResult = 0.0;for(int i = 0;i < vectorA.size();i++){dotProductResult += vectorA.get(i) * vectorB.get(i);}return dotProductResult/(sqrtA*sqrtB);}// 计算向量平方和的开方private static double countSquareSum(List<Double> vector){double result = 0.0;for (Double value : vector) {result += value*value;}return Math.sqrt(result);}}

对 MyTextComparator 进行测试:

下图为余弦函数图像,x轴表示角度,y轴表示余弦值。由图可知,夹角定义域在[0,180]时,余弦函数是单调递减的。这是符合“两向量间夹角越大,它们之间越不相似”这一基本前提的。所以,可以使用余弦值作为向量相似度的一个衡量:余弦值越接近于1,两向量相似度越高;反之,余弦值越接近于-1,两向量相似度越低。

基于这个原理,观察我们测试 MyTextComparator 的结果,发现得到的结果还是很符合直观印象的:文本2和其他文本都很不相似,文本0和文本3非常相似。

基于Java的文本相似度计算相关推荐

  1. 【NLP实战】基于ALBERT的文本相似度计算

    实战是学习一门技术最好的方式,也是深入了解一门技术唯一的方式.因此,NLP专栏推出了实战专栏,让有兴趣的同学在看文章之余也可以自己动手试一试. ALBERT是一个比BERT要轻量,效果更好的模型,本篇 ...

  2. 余弦定理的应用:基于文字的文本相似度计算

    最近由于工作项目,需要判断两个txt文本是否相似,于是开始在网上找资料研究,因为在程序中会把文本转换成String再做比较,所以最开始找到了这篇关于 距离编辑算法 Blog写的非常好,受益匪浅. 于是 ...

  3. Jaccard文本相似度计算 Java程序

    本文作者:合肥工业大学 管理学院 钱洋 email:1563178220@qq.com 内容可能有不到之处,欢迎交流. Jaccard相似系数 两个集合A和B交集元素的个数在A.B并集中所占的比例,称 ...

  4. Java实现标题相似度计算,文本内容相似度匹配,Java通过SimHash计算标题文本内容相似度

     目录 一.前言 二.关于SimHash 补充知识 一).什么是海明距离 二).海明距离的应用 三).什么是编辑距离 三.SimHash算法的几何意义和原理 一).SimHash算法的几何意义 二). ...

  5. 基于预训练词向量的文本相似度计算-word2vec, paddle

    文章目录 0. 前言 1. 余弦相似度算子 2. 示例代码并验证 3. 基于词向量的文本相似度 3.1 读取word2vec文件 3.2 定义模型 3.3 运行模型 3.4 根据分数降序排列 3.5 ...

  6. java 余弦定理_文本相似度计算之余弦定理

    前言 余弦相似度,又称为余弦相似性,是通过计算两个向量的夹角余弦值来评估他们的相似度.余弦相似度将向量根据坐标值,绘制到向量空间中.用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小.余弦值 ...

  7. Google开源word2vec,文本相似度计算工具

    Google开源word2vec,文本相似度计算工具 谷歌已经使用Deep Learning技术开发了许多新方法来解析语言,目前,谷歌开源了一款基于Deep Learning的学习工具--word2v ...

  8. sklearn tfidf求余弦相似度_【基础算法 】文本相似度计算

    在自然语言处理中,文本相似度是一种老生常谈而又应用广泛的基础算法模块,可用于地址标准化中计算与标准地址库中最相似的地址,也可用于问答系统中计算与用户输入问题最相近的问题及其答案,还可用于搜索中计算与输 ...

  9. 中文文本相似度计算工具集

    欢迎大家关注我们的网站和系列教程:http://www.tensorflownews.com/,学习更多的机器学习.深度学习的知识! 一.基本工具集 1.分词工具 a.jieba 结巴中文分词 htt ...

最新文章

  1. ajax 入参为list_ajax传递给后台数组参数方式
  2. Unable to resolve target 'android-5'
  3. mySQL数据库中的备份代码_MySQL中的备份数据库
  4. android 进度条 代码,Android 进度条使用详解及示例代码
  5. 10_Influxdb+Grafana监控Mysql
  6. DataReader对象的基本使用 c#
  7. java:去除字符串中空格 、 oracle (+) 、 mysql中数值运算符和函数
  8. 拓端tecdat|Python对商店数据进行lstm和xgboost销售量时间序列建模预测分析
  9. Java生成验证码合集(一)简单版
  10. 作战军事环境仿真系统软件解决方案
  11. 分享灵动微MM32F3270微控制器的音频类产品参考方案
  12. git大坑---cleanup
  13. 【“科大讯飞杯”第十七届同济大学】A 张老师和菜哭武的游戏
  14. 618买什么运动装备、最值得入手的运动装备合集
  15. AE502 112种创意视频字幕动画呼出线框文字标题效果包括PR预设与扩展脚本ae模板
  16. PS软件操作应用—文字特效
  17. MySQL 2019最全的国家地区代码、手机号正则验证,覆盖191个国家和地区
  18. django多任务开启rabbitmq,并进行声明队列、发送、阻塞监听消息
  19. NHibernate in Action(第一章1.2)
  20. 【树状数组】【P5069】[Ynoi2015]纵使日薄西山

热门文章

  1. 立创商城pcb封装导入
  2. 小米air2se耳机只有一边有声音怎么办_小米无线蓝牙耳机Air2 SE——性价比背后的妥协之作...
  3. UML之顺序图(时序图)
  4. 【kali-权限提升】(4.2.3)社会工程学工具包:二维码组合攻击
  5. java实现马赛克,java如何用Processing生成马赛克风格的图像
  6. iOS开发 适配iOS10
  7. web的首屏加载优化
  8. 如何用光盘映像文件重装服务器系统,光盘映像文件怎么安装,小编教你光盘映像文件怎么安装系统...
  9. 感悟 | 电影《你的名字》
  10. Adversarial Semantic Alignment for Improved Image Captions