一个简单的拼写检查器

记得以前就看过这篇文章:How to write a spelling corrector,文章将贝叶斯原理运用于拼写检查,二十几行简单的Python的代码就实现了一个拼写检查器。

感叹数学的美妙之外,也很喜欢类似这样的文章,将一个问题的原理讲清楚,再配上一些代码实例做说明,从小见大,从浅入深,对人很有启发。

这里简单的介绍下基于贝叶斯原理的拼写检查原理,再给出一个java版和C#版的实现。

拼写检查器的原理:给定一个单词,选择和它最相似的拼写正确的单词,需要使用概率论,而不是基于规则的判断。给定一个词 w,在所有正确的拼写词中,我们想要找一个正确的词 c, 使得对于 w 的条件概率最大, 也就是说:

argmaxc P(c|w)

按照 贝叶斯理论 上面的式子等价于:

argmaxc P(w|c) P(c) / P(w)

因为用户可以输错任何词, 因此对于任何 c 来讲, 出现 w 的概率 P(w) 都是一样的, 从而我们在上式中忽略它, 写成:

argmaxc P(w|c) P(c)

这个式子有三个部分, 从右到左, 分别是:

1. P(c), 文章中出现一个正确拼写词 c 的概率, 也就是说, 在英语文章中, c 出现的概率有多大呢? 因为这个概率完全由英语这种语言决定, 我们称之为做语言模型. 好比说, 英语中出现 the 的概率  P(‘the‘) 就相对高, 而出现  P(‘zxzxzxzyy‘) 的概率接近0(假设后者也是一个词的话).

2. P(w|c), 在用户想键入 c 的情况下敲成 w 的概率. 因为这个是代表用户会以多大的概率把 c 敲错成 w, 因此这个被称为误差模型.

3. argmaxc, 用来枚举所有可能的 c 并且选取概率最大的, 因为我们有理由相信, 一个(正确的)单词出现的频率高, 用户又容易把它敲成另一个错误的单词, 那么, 那个敲错的单词应该被更正为这个正确的.

最终计算的就是argmaxc P(w|c) P(c),在原文中,以“编辑距离”的大小来确定求最大概率的优先级: 编辑距离为1的正确单词比编辑距离为2的优先级高, 而编辑距离为0的正确单词优先级比编辑距离为1的高。

java版实现:

public class SpellCorrect {
    private final HashMap<String, Integer> nWords = new HashMap<String, Integer>();

    //读取语料库,存储在nWords中
    public SpellCorrect(String file) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(file));
        Pattern p = Pattern.compile("\\w+");
        for(String temp = ""; temp != null; temp = in.readLine()){
            Matcher m = p.matcher(temp.toLowerCase());
            while(m.find())
                nWords.put((temp = m.group()), nWords.containsKey(temp) ? nWords.get(temp) + 1 : 1);
        }
        in.close();
    }

    //和word编辑距离为1的单词
    private final HashSet<String> edits(String word){
        HashSet<String> result = new HashSet<>();
        for(int i=0; i < word.length(); ++i)   //删除
            result.add(word.substring(0, i) + word.substring(i+1));
        for(int i=0; i < word.length()-1; ++i) //交换
            result.add(word.substring(0, i) + word.substring(i+1, i+2) + word.substring(i, i+1) + word.substring(i+2));
        for(int i=0; i < word.length(); ++i)   //替换
            for(char c=‘a‘; c <= ‘z‘; ++c) result.add(word.substring(0, i) + String.valueOf(c) + word.substring(i+1));
        for(int i=0; i <= word.length(); ++i)  //插入
            for(char c=‘a‘; c <= ‘z‘; ++c) result.add(word.substring(0, i) + String.valueOf(c) + word.substring(i));
        return result;
    }

    public final String correct(String word) {
        if(nWords.containsKey(word)) //如果在语料库中,直接返回
            return word;
        HashSet<String> set = edits(word);
        HashMap<Integer, String> candidates = new HashMap<Integer, String>();
        for(String s : set) //在语料库中找编辑距离为1且出现次数最多的单词为正确单词
            if(nWords.containsKey(s))
                candidates.put(nWords.get(s),s);
        if(candidates.size() > 0)
            return candidates.get(Collections.max(candidates.keySet()));
        for(String s : set) //在语料库中找编辑距离为2且出现次数最多的单词为正确单词
            for(String w : edits(s))
                if(nWords.containsKey(w))
                    candidates.put(nWords.get(w),w);
        return candidates.size() > 0 ? candidates.get(Collections.max(candidates.keySet())) : word;
    }

    public static void main(String args[]) throws IOException {
        SpellCorrect spellCorrect = new SpellCorrect("big.txt");
        System.out.println(spellCorrect.correct("spel"));
        System.out.println(spellCorrect.correct("speling"));
    }
}

C#版实现:

namespace SpellCorrect
{
    class Program
    {
        const string Alphabet = "abcdefghijklmnopqrstuvwxyz";

        static IEnumerable<string> Edits1(string w)
        {
            // Deletion
            return (from i in Enumerable.Range(0, w.Length)
                    select w.Substring(0, i) + w.Substring(i + 1))
                // Transposition
             .Union(from i in Enumerable.Range(0, w.Length - 1)
                    select w.Substring(0, i) + w.Substring(i + 1, 1) +
                           w.Substring(i, 1) + w.Substring(i + 2))
                // Alteration
             .Union(from i in Enumerable.Range(0, w.Length)
                    from c in Alphabet
                    select w.Substring(0, i) + c + w.Substring(i + 1))
                // Insertion
             .Union(from i in Enumerable.Range(0, w.Length + 1)
                    from c in Alphabet
                    select w.Substring(0, i) + c + w.Substring(i));
        }

        static string Correct(string word, Dictionary<string,int> nWords)
        {
            Func<IEnumerable<string>, IEnumerable<string>> nullIfEmpty = c => c.Any() ? c : null;

            var candidates =
                nullIfEmpty(new[] { word }.Where(nWords.ContainsKey))
                ?? nullIfEmpty(Edits1(word).Where(nWords.ContainsKey))
                ?? nullIfEmpty((from e1 in Edits1(word)
                                from e2 in Edits1(e1)
                                where nWords.ContainsKey(e2)
                                select e2).Distinct());

            return candidates == null
                ? word
                : (from cand in candidates
                   orderby (nWords.ContainsKey(cand) ? nWords[cand] : 1) descending
                   select cand).First();
        }

        static void Main(string[] args)
        {
            var nWords = (from Match m in Regex.Matches(File.ReadAllText("big.txt").ToLower(), "[a-z]+")
                          group m.Value by m.Value)
                     .ToDictionary(gr => gr.Key, gr => gr.Count());

            Console.WriteLine(Correct("spel",nWords));
            Console.WriteLine(Correct("speling",nWords));

            Console.ReadKey();
        }
    }
}

两个版本的实现都参考了网上其他人的代码,略有修改。

优秀的代码看起来就是很舒服!

参考:

http://norvig.com/spell-correct.html

http://blog.youxu.info/spell-correct.html

时间: 2024-10-25 22:02:49

一个简单的拼写检查器的相关文章

《Yii2 By Example》第2章:创建一个简单的新闻阅读器

第2章 创建一个简单的新闻阅读器 本章内容包含:创建第一个控制器,用于展示新闻条目列表和详情:学习控制器和视图之间的交互:自定义视图的布局. 本章结构如下: 创建控制器和动作 创建用于展示新闻列表的视图 控制器是如何将数据传送到视图的 例子--创建一个控制器,展示静态新闻条目列表和详情 将常用视图内容分割成多个可复用视图 例子--在视图中进行部分渲染 创建静态页面 在视图和布局之前共享数据 例子--根据URL参数更换布局背景 使用动态模块布局 例子--添加展示广告信息的动态盒 使用多个布局 例子

自己动手实现一个简单的JSON解析器

1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐.所以作为开发人员,如果有兴趣的话,还是应该深入了解一下 JSON 相关的知识.本着探究 JSON 原理的目的,我将会在这篇文章中详细向大家介绍一个简单的JSON解析器的解析流

使用Python制作一个简单的刷博器

呵呵,不得不佩服Python的强大,寥寥几句代码就能做一个简单的刷博器. import webbrowser as web import time import os count=0 while count<10: count=count+1 #你要刷的博客 web.open_new_tab("http://www.cnblogs.com/smiler/archive/2010/04/20/1716418.html#2856973") time.sleep(1) else: os

Arachnid包含一个简单的HTML剖析器能够分析包含HTML内容的输入流

Arachnid是一个基于Java的web spider框架.它包含一个简单的HTML剖析器能够分析包含HTML内容的输入流.通过实现Arachnid的子类就能够开发一个简单的Web spiders并能够在Web站上的每个页面被解析之后增加几行代码调用. Arachnid的下载包中包含两个spider应用程序例子用于演示如何使用该框架. http://sourceforge.net/projects/arachnid/

C++ Primer 学习笔记_29_操作符重载与转换(4)--转换构造函数和类型转换运算符归纳、operator new 和 operator delete 实现一个简单内存泄漏跟踪器

C++ Primer 学习笔记_29_操作符重载与转换(4)--转换构造函数和类型转换运算符归纳.operator new 和 operator delete 实现一个简单内存泄漏跟踪器 一.转换构造函数 可以用单个实参来调用的构造函数定义从形参类型到该类型的一个隐式转换.如下: class Integral { public: Integral (int = 0); //转换构造函数 private: int real; }; Integral A = 1; //调用转换构造函数将1转换为In

使用lua实现一个简单的事件派发器

设计一个简单的事件派发器,个人觉得最重要的一点就是如何保证事件派发过程中,添加或删除同类事件,不影响事件迭代顺序和结果,只要解决这一点,其它都好办. 为了使用pairs遍历函数,重写了pairs(lua 5.2以上版本不需要): stdext.lua local _ipairs = ipairs function ipairs(t) local mt = getmetatable(t) if mt and mt.__ipairs then return mt.__ipairs(t) end re

贝叶斯拼写检查器

本拼写检查器是基于朴素贝叶斯的基础来写的,贝叶斯公式以及原理就不在详述.直接上代码 import re, collections def words(text): return re.findall('[a-z]+', text.lower()) def train(features): model = collections.defaultdict(lambda : 1) for f in features: model[f] += 1 return model alphabet = 'abc

实现了一个简单的cage变形器

今天实现了一个简单变形器,可以用一个网格的形状影响另一个网格的形状. 如图,蓝色网格的形状被灰色网格操控. 当前的算法非常简单,就是计算蓝色网格每个点到灰色网格每个点的距离,以距离x次方的倒数作为权重. 没有使用均值坐标等高级算法. cage deformer节点的参数如下: max neighbour是影响蓝色网格每个点的最大灰色网格顶点数 weight power是计算权重的参数 下一步可以利用这个变形器尝试复杂动画(例如骨骼绑定的角色)的有限元模拟.具体来说就是让骨骼参数驱动角色的精细网格

用贝叶斯实现拼写检查器

贝叶斯公式 p(A|D)=p(A)*p(D|A)/p(D); 可以应用于垃圾邮件的过滤和拼写检查 例如:对于拼写检查,写出一个单词D,判断该单词为正确单词A的概率.为上述条件概率的描述. 其中p(A)为先验概率,可以根据现有的数据库中的单词,来获得A单词的概率p(A).由于正确的单词不仅仅有A,还有可能有A1,A2.... 最终比较p(A1|D),p(A2|D),p(A3|D)...由于分母比较时相同,可以只比较分子p(A)*p(D|A) p(A|D)正比于p(A)*p(D|A) 分别计算p(A