从编辑距离、BK树到文本纠错
搜索引擎里有一个很重要的话题,就是文本纠错,主要有两种做法,一是从词典纠错,一是分析用户搜索日志,今天我们探讨使用基于词典的方式纠错,核心思想就是基于编辑距离,使用BK树。下面我们来逐一探讨:
编辑距离
1965年,俄国科学家Vladimir
Levenshtein给字符串相似度做出了一个明确的定义叫做Levenshtein距离,我们通常叫它“编辑距离”。
字符串A到B的编辑距离是指,只用插入、删除和替换三种操作,最少需要多少步可以把A变成B。例如,从FAME到GATE需要两步(两次替换),从GAME到ACM则需要三步(删除G和E再添加C)。Levenshtein给出了编辑距离的一般求法,就是大家都非常熟悉的经典动态规划问题。
class LevenshteinDistanceFunction {
private final boolean isCaseSensitive;
public LevenshteinDistanceFunction(boolean isCaseSensitive) {
this.isCaseSensitive = isCaseSensitive;
}
public int distance(CharSequence left, CharSequence right) {
int leftLength = left.length(), rightLength = right.length();
// special cases.
if (leftLength == 0)
return rightLength;
if (rightLength == 0)
return leftLength;
// Use the iterative matrix method.
int[] currentRow = new int[rightLength + 1];
int[] nextRow = new int[rightLength + 1];
// Fill first row with all edit counts.
for (int i = 0; i <= rightLength; i++)
currentRow[i] = i;
for (int i = 1; i <= leftLength; i++) {
nextRow[0] = i;
for(int j = 1; j <= rightLength; j++) {
int subDistance = currentRow[j - 1]; // Distance without insertions or deletions.
if (!charEquals(left.charAt(i - 1), right.charAt(j - 1), isCaseSensitive))
subDistance++; // Add one edit if letters are different.
nextRow[j] = Math.min(Math.min(nextRow[j - 1], currentRow[j]) + 1, subDistance);
}
// Swap rows, use last row for next row.
int[] t = currentRow;
currentRow = nextRow;
nextRow = t;
}
return currentRow[rightLength];
}
}
BK树
编辑距离的经典应用就是用于拼写检错,如果用户输入的词语不在词典中,自动从词典中找出编辑距离小于某个数n的单词,让用户选择正确的那一个,n通常取到2或者3。
这个问题的难点在于,怎样才能快速在字典里找出最相近的单词?可以像 使用贝叶斯做英文拼写检查(c#) 里是那样,通过单词自动修改一个单词,检查是否在词典里,这样有暴力破解的嫌疑,是否有更优雅的方案呢?
1973年,Burkhard和Keller提出的BK树有效地解决了这个问题。BK树的核心思想是:
令d(x,y)表示字符串x到y的Levenshtein距离,那么显然:
d(x,y) = 0 当且仅当 x=y (Levenshtein距离为0 <==> 字符串相等)
d(x,y) = d(y,x) (从x变到y的最少步数就是从y变到x的最少步数)
d(x,y) + d(y,z) >= d(x,z) (从x变到z所需的步数不会超过x先变成y再变成z的步数)
最后这一个性质叫做三角形不等式。就好像一个三角形一样,两边之和必然大于第三边。
BK建树
首先我们随便找一个单词作为根(比如GAME)。以后插入一个单词时首先计算单词与根的Levenshtein距离:如果这个距离值是该节点处头一次出现,建立一个新的儿子节点;否则沿着对应的边递归下去。例如,我们插入单词FAME,它与GAME的距离为1,于是新建一个儿子,连一条标号为1的边;下一次插入GAIN,算得它与GAME的距离为2,于是放在编号为2的边下。再下次我们插入GATE,它与GAME距离为1,于是沿着那条编号为1的边下去,递归地插入到FAME所在子树;GATE与FAME的距离为2,于是把GATE放在FAME节点下,边的编号为2。
BK查询
如果我们需要返回与错误单词距离不超过n的单词,这个错误单词与树根所对应的单词距离为d,那么接下来我们只需要递归地考虑编号在d-n到d+n范围内的边所连接的子树。由于n通常很小,因此每次与某个节点进行比较时都可以排除很多子树。
可以通过下图(来自 超酷算法(1):BK树 (及个人理解))理解:
BK 实现
知道了原理实现就简单了,这里从github找一段代码
建树:
public boolean add(T t) {
if (t == null)
throw new NullPointerException();
if (rootNode == null) {
rootNode = new Node<>(t);
length = 1;
modCount++; // Modified tree by adding root.
return true;
}
Node<T> parentNode = rootNode;
Integer distance;
while ((distance = distanceFunction.distance(parentNode.item, t)) != 0
|| !t.equals(parentNode.item)) {
Node<T> childNode = parentNode.children.get(distance);
if (childNode == null) {
parentNode.children.put(distance, new Node<>(t));
length++;
modCount++; // Modified tree by adding a child.
return true;
}
parentNode = childNode;
}
return false;
}
查找:
public List<SearchResult<T>> search(T t, int radius) {
if (t == null)
return Collections.emptyList();
ArrayList<SearchResult<T>> searchResults = new ArrayList<>();
ArrayDeque<Node<T>> nextNodes = new ArrayDeque<>();
if (rootNode != null)
nextNodes.add(rootNode);
while(!nextNodes.isEmpty()) {
Node<T> nextNode = nextNodes.poll();
int distance = distanceFunction.distance(nextNode.item, t);
if (distance <= radius)
searchResults.add(new SearchResult<>(distance, nextNode.item));
int lowBound = Math.max(0, distance - radius), highBound = distance + radius;
for (Integer i = lowBound; i <= highBound; i++) {
if (nextNode.children.containsKey(i))
nextNodes.add(nextNode.children.get(i));
}
}
searchResults.trimToSize();
Collections.sort(searchResults);
return Collections.unmodifiableList(searchResults);
}
使用BK树做文本纠错
准备词典,18万的影视名称:
测试代码:
static void outputSearchResult( List<SearchResult<CharSequence>> results){
for(SearchResult<CharSequence> item : results){
System.out.println(item.item);
}
}
static void test(BKTree<CharSequence> tree,String word){
System.out.println(word+"的最相近结果:");
outputSearchResult(tree.search(word,Math.max(1,word.length()/4)));
}
public static void main(String[] args) {
BKTree<CharSequence> tree = new BKTree(DistanceFunctions.levenshteinDistance());
List<String> testStrings = FileUtil.readLine("./src/main/resources/act/name.txt");
System.out.println("词典条数:"+testStrings.size());
long startTime = System.currentTimeMillis();
for(String testStr: testStrings){
tree.add(testStr.replace(".",""));
}
System.out.println("建树耗时:"+(System.currentTimeMillis()-startTime)+"ms");
startTime = System.currentTimeMillis();
String[] testWords = new String[]{
"湄公河凶案",
"葫芦丝兄弟",
"少林足球"
};
for (String testWord: testWords){
test(tree,testWord);
}
System.out.println("测试耗时:"+(System.currentTimeMillis()-startTime)+"ms");
}
结果:
词典条数:18513
建树耗时:421ms
湄公河凶案的最相近结果:
湄公河大案
葫芦丝兄弟的最相近结果:
葫芦兄弟
少林足球的最相近结果:
少林足球
笑林足球
测试耗时:20ms
参考:
http://blog.csdn.net/tradymeky/article/details/40581547
https://github.com/sk-scd91/BKTree
https://www.cnblogs.com/data2value/p/5707973.html
作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
从编辑距离、BK树到文本纠错的更多相关文章
- 超酷算法-BK树
前几天无意间遇到一个博客,觉得写得挺好的,自己之前的时候有个不好的习惯,那就是遇到了好资源第一反应就是收藏起来然后却很少再看!!这是坏习惯,要改!于是今天就开始通读了,读的第二篇是BK树.觉得有点意思 ...
- 调用百度API进行文本纠错
毕设做的是文本纠错方面,然后今天进组见研究生导师 .老师对我做的东西蛮感兴趣.然后介绍自己现在做的一些项目,其中有个模块需要有用到文本纠错功能. 要求1:有多人同时在线编辑文档,然后文档功能有类似Wo ...
- 拼写纠错的利器,BK树算法
BK树或者称为Burkhard-Keller树,是一种基于树的数据结构,被设计于快速查找近似字符串匹配,比方说拼写纠错,或模糊查找,当搜索”aeek”时能返回”seek”和”peek”. 本文首先剖析 ...
- 【.net 深呼吸】将目录树转化为文本
大伙都知道,文件系统是树形结构的,有时候我们会想到把目录的层次结构变为纯文本形式,就像这样: ├─Windows-universal-samples-master │ ├─Samples │ │ ├─ ...
- HDU 4323 Magic Number(编辑距离DP)
http://acm.hdu.edu.cn/showproblem.php?pid=4323 题意: 给出n个串和m次询问,每个询问给出一个串和改变次数上限,在不超过这个上限的情况下,n个串中有多少个 ...
- paddle&蜜度 文本智能较对大赛经验分享(17/685)
引言 我之前参加了一个中文文本智能校对大赛,拿了17名,虽然没什么奖金但好歹也是自己solo拿的第一个比较好的名次吧,期间也学到了一些BERT应用的新视角和新的预训练方法,感觉还挺有趣的,所以在这里记 ...
- 后缀树 & 后缀数组
后缀树: 字符串匹配算法一般都分为两个步骤,一预处理,二匹配. KMP和AC自动机都是对模式串进行预处理,后缀树和后缀数组则是对文本串进行预处理. 后缀树的性质: 存储所有 n(n-1)/2 个后缀需 ...
- ztree树的模糊搜索功能
在做机场项目的时候,业务为一个input框,点击的时候出现一个下拉树,这个下拉树是所有的设备,由于设备太多,加上分了区域,为了更好的用户体验,设计一个模糊搜索的功能,方便用户进行选择 具体实现过程如下 ...
- fp-growth树创建代码及详细注释
事务集过滤重排: #FP树节点结构 class treeNode: def __init__(self,nameValue,numOccur,parentNode): self.name=nameVa ...
随机推荐
- es6零基础学习之构建脚本(二)
编译器打开你的es6项目 首先:创建我们的第一个脚本,tasks/util/args.js 在文件里面要先引入一个包,处理命令行参数 import yargs from 'yargs'; / ...
- 快速高效掌握企业级项目中的Spring面向切面编程应用,外带讲面试技巧
Spring面向切面编程(AOP)是企业级应用的基石,可以这样说,如果大家要升级到高级程序员,这部分的知识必不可少. 这里我们将结合一些具体的案例来讲述这部分的知识,并且还将给出AOP部分的一些常见面 ...
- Java爬虫
作为一位Java爬虫的初学者,分享一下自己的心得.所用到的jar包 org.codehaus.jettison.jar jsoup-1.7.3.jar个人认为爬虫的实现机制:获取Docume对象-&g ...
- visual studio 2015 warning MSB3246
在我们很高兴的按下 本地计算机运行 按钮,希望看到我们程序运行的时候,垃圾vs就告诉我们,你的程序出现了问题,问题就是: warning MSB3246: 解析的文件包含错误图像.无元数据或不可访问. ...
- win10 uwp 右击浮出窗在点击位置
本文主要让MenuFlyout出现在我们右击位置. 我们一般使用的MenuFlyout写在前台,写在Button里面,但是可能我们的MenuFlyout显示的位置和我们想要的不一样. 通过使用后台写S ...
- 【转】地址空间、内核空间、IO地址空间
http://blog.csdn.net/wuxinke_blog/article/details/8769131 有这么一系列的问题,是否在困扰着你:用户程序编译连接形成的地址空间在什么范围内?内核 ...
- 【ASP.NET MVC 学习笔记】- 08 URL Routing
本文参考:http://www.cnblogs.com/willick/p/3343105.html 1.URL Routing告诉MVC如何正确的定位Controller和Action. 2.URL ...
- Yii2之类自动加载
在yii中,程序中需要使用到的类无需事先加载其类文件,在使用的时候才自动定位类文件位置并加载之,这么高效的运行方式得益于yii的类自动加载机制. Yii的类自动加载实际上使用的是PHP的类自动加载,所 ...
- 从项目中总结的js知识点
1. 数字字符串和数字进行比较可以得出正确结果,却不能正确判断是否在一个数字数组中.如以下程序: var s = '8', n = 8, arr = [1,2,8,9]; console.log(s= ...
- PHP设计模式三:原型设计模式
一.什么是原型设计模式 原型设计模式使用一种克隆技术来复制实例化的对象,新对象是通过复制原型实例创建的.原型设计模式的目的是通过使用克隆以减少 实例化对象的开销. 在原型设计模式中,Client类是不 ...