一、 背景

在问题检索中,依赖文本相似度给用户做推荐问题,假设1.0分为满分,那么:

1.0分表示完全匹配:可以将问题准确推送给用户

0.8分表示高度相似:可以将问题推荐给用户

0.6分表示低度相似:......

根据这样的规则对用户的检索做出回应。其实Lucene基于TF-IDF改造的相关度排序算法也有分值,但是和业务所需要的相似度不贴合,所以其得分只作为第一步结果筛选依据。关于Lucene打分公式可以看看这篇文章,Lucene的源码也做过详细解析,Lucene检索源码解析(上)和Lucene检索源码解析(下),感兴趣的朋友可以研究一下。

二、莱文斯坦距离

文本相似度算法有很多,我这里选择的是编辑距离算法-莱文斯坦距离(Levenshtein)。它表示的是将一个字符串a变换为另一个字符串b,所需要的字符插入、删除、替换的次数。

对于字符串a和b,分别用|a|和|b|代表其长度,那么他们的莱文斯坦距离表示为:,它符合:

它表示,若a或b有一个是空串,那么距离为非空串的长度(max运算);否则,进入min运算,三个公式从上到下分别表示,从a中删除一个字符、往a中添加一个字符、字符替换。

第三个公式中的 ,是一个指示函数,它表示当时为1,反之等于0。意思就是如果字符相同,则不用替换,如果字符不同,则需要1次替换。

比如将“kitten”一字转成“sitting”的莱文斯坦距离为3(例子来自维基):

  1. kitten → sitten (k→s)
  2. sitten → sittin (e→i)
  3. sittin → sitting (插入g)

关于莱文斯坦距离的详细内容,可以看看维基定义,这里就不赘述了。

三、相似度计算

结合业务之后,计算距离就不再是字符变更,而是词。对于两个文本,要先对内容做分词,去除停顿词等操作。分词是词法分析中最基本的任务,它把一个语句拆分为多个词,特征提取一般也要建立在分词的基础上。其可以基于词典、基于统计或者基于规则等,算法也比较多,比如最大匹配、隐马尔科夫模型等,不过分词不是本章节的重点。

在分词之后,莱文斯坦距离的计算目标就变成了:将一个词列表变换为另一个词列表,所需要的词删除、添加、替换的次数(操作从字符变成了词)。但是这里要注意的是分词结果的顺序问题,在一些情况下,词的顺序是在一定程度上代表了语义的,比如:

分词1:我 吃饭 后 回家

分词2:我 回家 后 吃饭

字符串本身是一个整体,按照字符拼接顺序处理即可,但是分词结果本身是一个词的列表,所以要注意这个问题。

结合分词之后,加入同义词的逻辑就是:判断需要一次替换操作的条件为:!item1.equals(items2) && 不是同义词(item1,item2),即指示函数变更为:

得到距离之后,按照要求做一个归一化处理即可,所以我们需要考虑的问题就是如何归一化处理和在兼容内存消耗下较高效率的判断两个词是否为同义词。

四、同义词判定

出于对存储和读取效率的考虑,主要就是找到一个适合的数据结构实现字典功能,主要实现有:有序列表(查询时使用二分法查找)、二叉排序树、跳表、HashMap、FST等,出于多方面因素考虑,项目中使用的lucene中也有开源的FST实现,FST对于内存压缩优势较大,而查询效率也较高,所以选择使用FST作为字典实现比对同义词。

注:FST,即Finite State Transducer,有穷状态转换器,其通过单词前后缀的重复利用,最终生成一个无环图,在很大的程度上减少了内存消耗。FST表示为字典结构:FST<key,value>时,只需要O(lengthOf(key))的时间复杂度。关于FST的细节,可自行查阅。

在当前实现中,我们已经根据同义词库创建好了FST,每个都词有一个hash id,在初始化同义词库时,对于一个词,使用几个字节存储其同义词的数量,然后将它们的hash id按照规则存储在一起,方便读取。这样对于一个输入词,我们能快速获取到其同义词的hash id列表(多个同义词),然后根据目标词的hash id进行比对即可。查询同义词hash id列表的相关代码片段如下:

ByteArrayDataInput reader = new ByteArrayDataInput(bytesRef.bytes, bytesRef.offset, bytesRef.length);
//获取长度,如果多个字节存储了长度,在readVInt的时候自动递增position
int size = reader.readVInt();
size >>= 1;
if (logger.isDebugEnabled()) {logger.debug("[FST跟踪]text:{},同义词数量:{}", text, size);
}//获取所有同义词的hash id
int[] results = new int[size];
int index = 0;
while (!reader.eof()) {results[index++] = reader.readVInt();
}
return results;

注:同义词库记录了同义词关系,可以是文件或者数据表等等,其表征了哪些词互为同义词。比如在文件中,将属于同义词的词列在同一排:

晚上 黑夜 夜间

吃饭 进食 干饭

在指示函数的实现中,根据hash id进行比对就可以了:

private boolean isSynonyms(int[] outputs, int hash) {//如果同义词较多的话,可以优化匹配方式,这里演示就直接遍历,O(n)if (outputs != null && hash > 0) {for (int output : outputs) {if (output == hash) {return true;}}}return false;}

五、归一化算法

编辑距离的结果是距离,表征为一个数值,但是我们需要的是一个分值score(0.0<=score<=1.0)来表征相似度,所以需要做归一化处理。这里首先考虑使用线性函数进行归一化处理,标准的线性归一化处理公式为:。将原始数据进行等比例缩放。

在此处的距离中,简化处理为:。其中cost为消耗步数,maxSize(word)为最长词列表长度。对于两个语句,变换距离(cost)越大,就代表越不相似,所以最终得分公式为:

现在需要考虑另外一个问题,编辑距离是一个量化的结果,其本身表征的意义和我们的直观感受结果可能有所不同,特别是在极端情况下。比如以下两个语句:

语句:回家吃饭  分词结果:回家 吃饭

语句:回家种花 分词结果:回家 种花

根据编辑距离算法,他们的相似度得分为0.5分,从理性上讲,他们有一半的词是相同的,那么0.5分可以理解。工程化后,从感性上讲,如果语句很长(分词列表很长),0.5分的相似度似乎并不足以我们推荐给用户。但是对于上面这种情况,虽然是0.5分,但是由于语句本身很短,也是值得我们推荐给用户的。也就需要对这种情况特殊处理(提高其分值),但是不能影响其它情况,所以需要设计贴合业务的归一化算法。由于新算法业务强相关,这里就不贴出来了。

六、实现

    @Overridepublic double similarScore(List<String> words1, List<String> words2, Long companyId) throws Exception {if (words1 == null || words2 == null) {return 0.0D;}double costs;double maxLength;//首先判断极端情况的距离if (words1 == null) {costs = words2.size();maxLength = costs;} else if (words2 == null) {costs = words1.size();maxLength = costs;} else {//都不为空String[] s_segs = words1.toArray(new String[]{});words1.clear();String[] t_segs = words2.toArray(new String[]{});words2.clear();maxLength = Math.max(s_segs.length, t_segs.length);//使用两个一维数组迭代实现if (s_segs.length < t_segs.length) {// 为了节约内存,将元素较少的列表当做目标列表String[] tmp = s_segs;s_segs = t_segs;t_segs = tmp;}int s_length = s_segs.length;int t_length = t_segs.length;int pre[] = new int[t_length + 1]; //保存前一行记录int current[] = new int[t_length + 1]; //当前行记录int[] tmp;//用以交换pre和current//初始化第一行for (int i = 0; i < pre.length; i++) {pre[i] = i;}//迭代计算String s_w;String t_w;int cost;for (int i = 1; i <= s_length; i++) {//第一列的值设置为jcurrent[0] = i;s_w = s_segs[i - 1];//计算一行的值for (int j = 1; j <= t_length; j++) {t_w = t_segs[j - 1];//指示函数的实现if (s_w.equals(t_w) || isMutualSynonyms(companyId, s_w, t_w)) {cost = 0;} else {cost = 1;}//从新增、删除、替换中选取最小值current[j] = Math.min(Math.min(current[j - 1] + 1, pre[j] + 1), pre[j - 1] + cost);}//将当前行设置为前一行的值,为下次做准备tmp = pre;pre = current;current = tmp;}costs = pre[t_length];}return normalization(costs, maxLength);}private double normalization(double costs, double max) {return 1 - (costs / max);}

相似度算法--莱文斯坦距离加入同义词逻辑相关推荐

  1. 莱文斯坦距离(编辑距离)算法 (Levenshtein Distance Algorithm)

    什么是 莱文斯坦距离算法 (Levenshtein Distance Algorithm) ? Levenshtein Distance,莱文斯坦距离,通常被称为编辑距离(Edit Distance) ...

  2. 编辑距离算法【莱文斯坦距离、Levenshtein 算法】

    文章目录 算法概述: 应用 与其他编辑距离度量的关系 问题定义: 解析: 例题: 参考链接: 算法概述: 在信息论和计算机科学中,莱文斯坦距离是一种两个字符串序列的距离度量.形式化地说,两个单词的莱文 ...

  3. 动态规划——莱文斯坦距离

    文章出处:极客时间<数据结构和算法之美>-作者:王争.该系列文章是本人的学习笔记. 莱文斯坦距离 在搜索引擎中会有搜索词纠错的功能.这个功能背后的原理是编辑距离. 编辑距离 编辑距离是量化 ...

  4. LD(Levenshtein distance)莱文斯坦距离----编辑距离

    链接:https://ac.nowcoder.com/acm/contest/327/G 来源:牛客网 G处女座与复读机 题目描述 一天,处女座在牛客算法群里发了一句"我好强啊", ...

  5. 编辑距离——莱文斯坦距离(Levenshtein distance)

    在信息论和计算机科学中,莱文斯坦距离是一种两个字符串序列的距离度量.形式化地说,两个单词的莱文斯坦距离是一个单词变成另一个单词要求的最少单个字符编辑数量(如:删除.插入和替换).莱文斯坦距离也被称做编 ...

  6. java文本相似度算法

    import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; i ...

  7. 一种中文字符串相似度算法

    一种中文字符串相似度算法 概要 标记距离相似算法 扩展 概要 给定一个字符串a,在字符串列表B中找到与a最相似字符串b,或者让列表B按与a相似度排序.本文提出一种算法来较好的解决这个问题.并且该算法很 ...

  8. 动态规划 | 可以用在核酸检测的算法:莱文斯坦算法

    核酸检测经此一疫已经成为了人尽皆知的检测手段,但是你可曾想过经过仪器检测出来的 DNA序列是如何与正常情况下的 DNA 序列做对比的呢?面对整村的核酸检测结果,计算工作肯定需要电脑来完成,如果我们想知 ...

  9. 相似度算法和距离算法

    常见的距离算法和相似度(相关系数)计算方法 查看原文 摘要: 1.常见的距离算法 1.1欧几里得距离(Euclidean Distance)以及欧式距离的标准化(Standardized Euclid ...

最新文章

  1. oracle 11gr2 单机数据库使用asm,RHEL7上安装11gR2单机使用ASM存储搭建Physical Standby笔记...
  2. div模拟textarea自适应高度
  3. anconda安装及opencv配置
  4. 7月10日任务 添加自定义监控项目、配置邮件告警、测试告警、不发邮件的问题处理...
  5. python 获取文件名_真实需求 | Python+os+openpyxl 批量获取Excel的文件名和最大行数...
  6. chimerge算法matlab实现,有监督的卡方分箱算法
  7. sqlserver命令行修改用户登录密码
  8. 阻止路由跳转得方式_vue中路由跳转的三种方式 简洁易懂
  9. VXLAN详解(三)
  10. python类与对象-如何派生内置不可变类型并修其改实例化行为
  11. 用android编写使用按钮ImageButton和切换器ImageSwitcher
  12. HttpClient4
  13. 烽火服务器查询服务器型号,烽火服务器应该起的进程
  14. 最早的动态图匹配代表性算法-邻接点树(NNT)
  15. matlab批量修改图片的大小_matlab批量修改图片大小
  16. 6月6日重庆 减肥美容、无痕线雕提升技术精品班 (顾春英)
  17. 苹果手机黑屏怎么办,苹果手机不能开机怎么办
  18. java如何等待异步结果_你如何等待所有异步调用在Java中完成?
  19. android计算器开源小项目代码(附安装包.apk)
  20. 免费领取丨精算与金融建模行业解决方案白皮书,不要错过!

热门文章

  1. CSS首字母下沉怎么设置?
  2. 华为、联想:外媒眼中的“中国制造”
  3. 什么是 5G CPE
  4. WinEdt的bib参考文献管理教程
  5. “诸神之眼”——Nmap端口扫描工具使用小手册
  6. jenkins中文语言设置
  7. dede - 栏目中判断
  8. Liquid Warping GAN 水记
  9. 不借助 matlab 内置函数,生撸均值方差模型
  10. python代理ip怎么写_python代理ip怎么写