`
frank1998819
  • 浏览: 751525 次
  • 性别: Icon_minigender_1
  • 来自: 南京
文章分类
社区版块
存档分类

拼写纠错的利器,BK树算法(转)

 
阅读更多

原作者: http://www.cnblogs.com/data2value/p/5707973.html
 

拼写纠错的利器,BK树算法

 

BK树或者称为Burkhard-Keller树,是一种基于树的数据结构,被设计于快速查找近似字符串匹配,比方说拼写纠错,或模糊查找,当搜索”aeek”时能返回”seek”和”peek”。

本文首先剖析了基本原理,并在后面给出了Java源码实现。

BK树在1973年由Burkhard和Keller第一次提出,论文在这《Some approaches to best match file searching》。这是网上唯一的ACM存档,需要订阅。更细节的内容,可以阅读这篇论文《Fast Approximate String Matching in a Dictionary》。

在定义BK树之前,我们需要预先定义一些操作。为了索引和搜索字典,我们需要一种比较字符串的方法。编辑距离( Levenshtein Distance)是一种标准的方法,它用来表示经过插入、删除和替换操作从一个字符串转换到另外一个字符串的最小操作步数。其它字符串函数也同样可接受(比如将调换作为原子操作),只要能满足以下一些条件。

现在我们观察下编辑距离:构造一个度量空间(Metric Space),该空间内任何关系满足以下三条基本条件:

  • d(x,y) = 0 <-> x = y (假如x与y的距离为0,则x=y)
  • d(x,y) = d(y,x) (x到y的距离等同于y到x的距离)
  • d(x,y) + d(y,z) >= d(x,z)

上述条件中的最后一条被叫做三角不等式(Triangle Inequality)。三角不等式表明x到z的路径不可能长于另一个中间点的任何路径(从x到y再到z)。看下三角形,你不可能从一点到另外一点的两侧再画出一条比它更短的边来。

编辑距离符合基于以上三条所构造的度量空间。请注意,有其它更为普遍的空间,比如欧几里得空间(Euclidian Space),编辑距离不是欧几里得的。既然我们了解了编辑距离(或者其它类似的字符串距离函数)所表达的度量的空间,再来看下Burkhard和Keller所观察到的关键结论。

假设现在我们有两个参数,query表示我们搜索的字符串,n为待查找的字符串与query距离满足要求的最大距离,我们可以拿任意字符串A来跟query进行比较,计算距离为d,因为我们知道三角不等式是成立的,则满足与query距离在n范围内的另一个字符转B,其与A的距离最大为d+n,最小为d-n。

推论如下:

d(query, B) + d(B, A) >= d(query, A),  即 d(query, B) + d(A,B) >= d    

                                                     -->  d(A,B) >= d - d(query, B) >= d - n

d(A, B) <= d(A,query) + d(query, B),   即 d(A, B) <= d + d(query, B) <= d + n

其实,还可以得到 d(query, A) + d(A,B) >= d(query, B)                   

                 --> d(A,B) >= d(query, B) - d(query, A) 

                 --> d(A,B) >= 1 - d >= 0 (query与B不等) 由于 A与B不是同一个字符串,所以d(A,B)>=1

所以, min{1, d - n} <= d(A,B) <= d + n,这是更为完整的结论。

由此,BK树的构造就过程如下:

每个节点有任意个子节点,每条边有个值表示编辑距离。所有子节点到父节点的边上标注n表示编辑距离恰好为n。比如,我们有棵树父节点是”book”和两个子节点”rook”和”nooks”,”book”到”rook”的边标号1,”book”到”nooks”的边上标号2。

从字典里构造好树后,无论何时你想插入新单词时,计算该单词与根节点的编辑距离,并且查找数值为d(neweord, root)的边。递归得与各子节点进行比较,直到没有子节点,你就可以创建新的子节点并将新单词保存在那。比如,插入”boon”到刚才上述例子的树中,我们先检查根节点,查找d(“book”, “boon”) = 1的边,然后检查标号为1的边的子节点,得到单词”rook”。我们再计算距离d(“rook”, “boon”)=2,则将新单词插在”rook”之后,边标号为2。

查询相似词如下:

计算单词与根节点的编辑距离d,然后递归查找每个子节点标号为d-n到d+n(包含)的边。假如被检查的节点与搜索单词的距离d小于n,则返回该节点并继续查询。

BK树是多路查找树,并且是不规则的(但通常是平衡的)。试验表明,1个查询的搜索距离不会超过树的5-8%,并且2个错误查询的搜索距离不会超过树的17-25%,这可比检查每个节点改进了一大步啊!需要注意的是,如果要进行精确查找,也可以非常有效地通过简单地将n设置为0进行。

英文原文:http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees

 

本文给出一个Java源码如下,相当简洁,注释清楚:

BK树的创建、添加、查询:

复制代码
package inteldt.todonlp.spellchecker;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * BK树,可以用来进行拼写纠错查询
 * 
 * 1.度量空间。
 * 距离度量空间满足三个条件:
 * d(x,y) = 0 <-> x = y (假如x与y的距离为0,则x=y)
 * d(x,y) = d(y,x) (x到y的距离等同于y到x的距离)
 * d(x,y) + d(y,z) >= d(x,z)  (三角不等式)
 * 
 * 2、编辑距离( Levenshtein Distance)符合基于以上三条所构造的度量空间
 * 
 * 3、重要的一个结论:假设现在我们有两个参数,query表示我们搜索的字符串(以字符串为例),
 *    n为待查找的字符串与query最大距离范围,我们可以拿一个字符串A来跟query进行比较,计
 *    算距离为d。根据三角不等式是成立的,则满足与query距离在n范围内的另一个字符转B,
 *    其余与A的距离最大为d+n,最小为d-n。
 *    
 *    推论如下:
 *    d(query, B) + d(B, A) >= d(query, A), 即 d(query, B) + d(A,B) >= d -->  d(A,B) >= d - d(query, B) >= d - n
 *    d(A, B) <= d(A,query) + d(query, B), 即 d(query, B) <= d + d(query, B) <= d + n
 *        其实,还可以得到  d(query, A) + d(A,B) >= d(query, B)  
 *                 -->   d(A,B) >= d(query, B) - d(query, A)  
 *                 -->  d(A,B) >= 1 - d >= 0 (query与B不等)  由于 A与B不是同一个字符串d(A,B)>=1
 *    所以,   min{1, d - n} <= d(A,B) <= d + n
 *    
 *    利用这一特点,BK树在实现时,子节点到父节点的权值为子节点到父节点的距离(记为d1)。
 *    若查找一个元素的相似元素,计算元素与父节点的距离,记为d, 则子节点中能满足要求的
 *    相似元素,肯定是权值在d - n <= d1 <= d + n范围内,当然了,在范围内,与查找元素的距离也未必一定符合要求。
 *    这相当于在查找时进行了剪枝,然不需要遍历整个树。试验表明,距离为1范围的查询的搜索距离不会超过树的5-8%,
 *    并且距离为2的查询的搜索距离不会超过树的17-25%。

 * 参见:
 * http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees(原文)
 * @author yifeng
 *
 */
public class BKTree<T>{
    private final MetricSpace<T> metricSpace;
    
    private Node<T> root;
    
    public BKTree(MetricSpace<T> metricSpace) {
        this.metricSpace = metricSpace;
    }
    
    /**
     * 根据某一个集合元素创建BK树
     * 
     * @param ms
     * @param elems
     * @return
     */
    public static <E> BKTree<E> mkBKTree(MetricSpace<E> ms, Collection<E> elems) {
        
        BKTree<E> bkTree = new BKTree<E>(ms);
        
        for (E elem : elems) {
            bkTree.put(elem);
        }
        
        return bkTree;
    }

    /**
     * BK树中添加元素
     * 
     * @param term
     */
    public void put(T term) {
        if (root == null) {
            root = new Node<T>(term);
        } else {
            root.add(metricSpace, term);
        }
    }
    
    /**
     * 查询相似元素
     * 
     * @param term
     *         待查询的元素
     * @param radius
     *         相似的距离范围
     * @return
     *         满足距离范围的所有元素
     */
    public Set<T> query(T term, double radius) {
        
        Set<T> results = new HashSet<T>();
        
        if (root != null) {
            root.query(metricSpace, term, radius, results);
        }
        
        return results;
    }
    
    private static final class Node<T> {
    
        private final T value;
        
        /**
         *  用一个map存储子节点
         */
        private final Map<Double, Node<T>> children;
        
        public Node(T term) {
            this.value = term;
            this.children = new HashMap<Double, BKTree.Node<T>>();
        }
        
        public void add(MetricSpace<T> ms, T value) {
            // value与父节点的距离
            Double distance = ms.distance(this.value, value);
            
            // 距离为0,表示元素相同,返回
            if (distance == 0) {
                return;
            }
            
            // 从父节点的子节点中查找child,满足距离为distance
            Node<T> child = children.get(distance);
            
            
            if (child == null) {
                // 若距离父节点为distance的子节点不存在,则直接添加一个新的子节点
                children.put(distance, new Node<T>(value));
            } else {
                // 若距离父节点为distance子节点存在,则递归的将value添加到该子节点下
                child.add(ms, value);
            }
        }
        
        public void query(MetricSpace<T> ms, T term, double radius, Set<T> results) {
            
            double distance = ms.distance(this.value, term);
            
            // 与父节点的距离小于阈值,则添加到结果集中,并继续向下寻找
            if (distance <= radius) {
                results.add(this.value);
            }
            
            // 子节点的距离在最小距离和最大距离之间的。
            // 由度量空间的d(x,y) + d(y,z) >= d(x,z)这一定理,有查找的value与子节点的距离范围如下:
            // min = {1,distance -radius}, max = distance + radius
            for (double i = Math.max(distance - radius, 1); i <= distance + radius; ++i) {
                
                Node<T> child = children.get(i);
                
                // 递归调用
                if (child != null) {
                    child.query(ms, term, radius, results);
                }
                
            }    
        }
    }
}
复制代码

 

距离度量方法接口:

复制代码
package inteldt.todonlp.spellchecker;

/**
 * 度量空间
 * 
 * @author yifeng
 *
 * @param <T>
 */
public interface MetricSpace<T> {

    double distance(T a, T b);
    
}
复制代码
编辑距离:
复制代码
package inteldt.todonlp.spellchecker;

/**
 * 编辑距离, 又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。
 * 该类中许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
 * 
 * 使用动态规划算法。算法复杂度:m*n。
 * 
 * @author yifeng
 *
 */
public class LevensteinDistance implements MetricSpace<String>{
    private double insertCost = 1;       // 可以写成插入的函数,做更精细化处理
    private double deleteCost = 1;       // 可以写成删除的函数,做更精细化处理
    private double substitudeCost = 1.5; // 可以写成替换的函数,做更精细化处理。比如使用键盘距离。
    
    public double computeDistance(String target,String source){
        int n = target.trim().length();
        int m = source.trim().length();
        
        double[][] distance = new double[n+1][m+1];
        
        distance[0][0] = 0;
        for(int i = 1; i <= m; i++){
            distance[0][i] = i;
        }
        for(int j = 1; j <= n; j++){
            distance[j][0] = j;
        }

        for(int i = 1; i <= n; i++){
            for(int j = 1; j <=m; j++){
                double min = distance[i-1][j] + insertCost;
                
                if(target.charAt(i-1) == source.charAt(j-1)){
                    if(min > distance[i-1][j-1]) 
                        min = distance[i-1][j-1];
                }else{
                    if(min > distance[i-1][j-1] + substitudeCost)
                        min = distance[i-1][j-1] + substitudeCost;
                }
                
                if(min > distance[i][j-1] + deleteCost){
                    min = distance[i][j-1] + deleteCost;
                }
                
                distance[i][j] = min;
            }
        }
        
        return distance[n][m];
    }

    @Override
    public double distance(String a, String b) {
        return computeDistance(a,b);
    }

    public static void main(String[] args) {
        LevensteinDistance distance = new LevensteinDistance();
        System.out.println(distance.computeDistance("你好","好你"));
    }
}
复制代码

有了以上三个类,下面写一个main函数玩起纠错功能:

复制代码
package inteldt.todonlp.spellchecker;

import java.util.Set;


/**
 * 拼写纠错
 * 
 * @author yifeng
 *
 */
public class SpellChecker {
public static void main(String args[]) {
        double radius = 1.5; // 编辑距离阈值
        String term = "helli"; // 待纠错的词
        
        // 创建BK树
        MetricSpace<String> ms = new LevensteinDistance();
        BKTree<String> bk = new BKTree<String>(ms);
        
        bk.put("hello");
        bk.put("shell");
        bk.put("holl");
        
        Set<String> set = bk.query(term, radius);
        System.out.println(set.toString());
        
    }
}

输出:[hello]
复制代码

 

如果您觉得博文对您有用,请随意打赏。您的鼓励是我前进的动力!

 

 
分类: NLP
 
好文要顶 关注我 收藏该文  
0
0
 
(请您对文章做出评价)
 
posted @ 2016-07-26 21:02 数据采集与NLP 阅读(51) 评论(0编辑 收藏
分享到:
评论

相关推荐

    拼写纠错 拼写检查 c++

    在IT领域,拼写纠错和拼写检查是自然语言处理(NLP)中的重要组成部分,尤其是在文本编辑器、搜索引擎和翻译软件中应用广泛。本文将详细介绍如何使用C++实现一个基本的英文拼写检查系统,主要涉及以下几个知识点: ...

    Java-work.rar_哈夫曼_哈夫曼压缩_拼写纠错_拼写纠错程序

    在本项目中,Java编程语言被用来实现两个关键功能:拼写纠错和哈夫曼压缩。下面我们将分别探讨这两个主题。 首先,哈夫曼编码是一种数据压缩算法,它基于字符出现频率来创建一种特殊的二进制编码。在这个“Java-...

    python实现bk树

    在实际应用中,BK树常用于搜索引擎的关键词索引、拼写检查、基因序列分析等领域。通过Python实现的BK树,可以帮助我们快速定位相似字符串,提升程序的效率和用户体验。当然,实现过程中还需要考虑异常处理和数据验证...

    快速单词拼写检错程序(字典树实现)

    快速单词拼写检错程序基于字典树的实现是一个高效的方法来检查文本中的拼写错误。这个Java程序利用了数据结构——字典树(Trie),也被称为前缀树或PAT树,它在处理字符串相关的搜索问题时表现出色。下面我们将深入...

    贝叶斯拼写纠错数据集

    机器学习课程中的一个案例 贝叶斯拼写纠错 数据集 big.txt

    lucene拼写纠错代码didyoumean

    在这个“lucene拼写纠错代码didyoumean”主题中,我们将深入探讨如何利用Lucene实现拼写纠错功能。 首先,Lucene的`SpellChecker`类是其拼写纠错功能的核心。这个类允许开发者构建一个拼写字典,并对比用户输入的...

    Lucene5学习之SpellCheck拼写纠错

    3. **纠错算法**: Lucene5使用了编辑距离算法(如Levenshtein距离)来计算单词间的相似度,找到最接近的正确拼写。 4. **建议排序与返回**: 根据相似度得分对纠正建议进行排序,然后返回前几个最可能的正确拼写。 *...

    python实现bk树(cython加速)

    - 字符串搜索:例如在拼写纠错、自动补全、DNA 序列比对等场景中, BK 树能快速找到可能的匹配项。 - 词汇索引:在大型词典或文本集合中,通过 BK 树可以快速查找和推荐类似单词。 通过结合 Python 的易用性和 ...

    MD-CSC多领域中文拼写纠错数据集.zip

    MD-CSC多领域中文拼写纠错数据集是一个专门用于训练和评估中文拼写纠错模型的重要资源。这个数据集涵盖了多个领域的文本,旨在提供一个全面、多样化的环境,以提升拼写纠错系统的性能和泛化能力。在AI和自然语言处理...

    ChatGPT技术在文本生成中的拼写纠错与语法纠错方法.docx

    ChatGPT技术在文本生成中扮演着至关重要的角色,尤其在拼写纠错和语法纠错方面。随着深度学习的持续进步,这些技术对于提升文本生成的准确性和流畅度至关重要。拼写纠错是解决ChatGPT生成文本中常见错误的第一步。...

    课时82贝叶斯拼写纠错实例_贝叶斯_贝叶斯;python_

    在本课程中,我们将深入探讨如何使用Python实现贝叶斯拼写纠错的实例。贝叶斯方法是一种统计学上的概率推理技术,它在各种领域,包括自然语言处理(NLP)中,都有着广泛的应用。拼写纠错是NLP中的一个基本任务,尤其...

    42丨动态规划实战:如何实现搜索引擎中的拼写纠错功能?1

    【动态规划】与【搜索引擎】在实际应用中的一个重要场景是实现拼写纠错功能。这个功能在用户输入搜索词时能够自动检测并纠正拼写错误,提高用户体验。在实现这一功能时,关键在于如何衡量两个字符串的相似度,这通常...

    英文拼写检查原理研究

    《英文拼写检查原理研究》 在信息技术领域,英文拼写检查是不可或缺的一部分,它广泛应用于各种文本编辑器、电子邮件客户端、搜索引擎等。拼写检查技术的发展极大地提升了用户输入文字的准确性,减少了因拼写错误...

    数据结构与算法 课程设计说明书 拼写检查器 trie树 字典树.doc

    数据结构与算法 课程设计说明书 拼写检查器 trie树 字典树.doc

    机器学习实战项目贝叶斯拼写检查器

    在本项目中,我们将深入探讨如何使用贝叶斯算法构建一个拼写检查器。这是一个非常实用的机器学习应用,尤其对于文本处理、自然语言处理(NLP)领域来说至关重要。贝叶斯算法因其简单而有效的特性,常被用于解决此类...

    基于Python的中文内容纠错算法-课程设计

    本项目是基于 Python 的中文文本内容纠错算法,基于jieba分词和中文词典技术实现。 中文文本纠错是针对中文文本拼写错误进行检测与纠正的一项工作,中文的文本纠错,应用场景很多,诸如输入法纠错、输入预测、ASR ...

    英文单词拼写混淆集:spell-errors.txt

    英文单词拼写混淆集:spell-errors.txt

    jbktree:实现为Java集合的通用BK树

    BK树的常见用例(但肯定不是唯一的用例)是字符串的“模糊匹配”或“拼写检查”。 在此用例中,我们将已知单词的列表添加到BK树中,然后可以在树中搜索查询词的一定内的单词。 为了演示此用例,我们可以从将单词...

    基于编辑距离的拼写矫正算法

    总的来说,基于编辑距离的拼写矫正算法是解决文本纠错问题的一个有效工具,通过理解和应用这一算法,我们可以提升系统的用户体验,减少因拼写错误带来的沟通障碍。在不断发展的自然语言处理技术中,这一算法仍然是...

Global site tag (gtag.js) - Google Analytics