贝叶斯定理拼写检查中的应用

阮一峰的博客
Google的研发总监Peter Norvig源码
在搜索引擎的应用中,拼写检查起着重要的作用:贝叶斯定理拼写检查中的应用_第1张图片

数学原理: 贝叶斯定理


贝叶斯定理由英国数学家贝叶斯 ( Thomas Bayes 1702-1761 ) 发展,用来描述两个条件概率之间的关系。
所谓”拼写检查”,就是在发生输入错误B的情况下,试图推断出A。从概率论的角度看,就是已知输入错误B,然后在若干个备选方案中,找出可能性最大的那个输入正确A,也就是求下面这个式子的最大值。
(比如lates应该被更正为late或者latest?),我们用概率决定把哪一个作为建议。我们从跟原始词相关的所有可能的正确拼写中找到可能性最大的那个拼写建议。
贝叶斯定理

事件A:要猜测输入正确事件的概率
事件B:现实已发生输入错误事件的概率

对于每一个A来说,输入错误B的概率相同,所以最大值可转换为

P(B|A)*P(A)

其中

P(A|B)是在拼写错误的情况下推断出拼写正确的情况

P(A)的含义是某个正确的词的出现”概率”,它可以用”频率”代替。如果我们有一个足够大的文本库,那么这个文本库中每个单词的出现频率,就相当于它的发生概率。某个词的出现频率越高,P(A)就越大。

P(B|A)的含义是,在试图拼写正确A的情况下,出现拼写错误B的概率。这需要统计数据的支持,但是为了简化问题,我们假设两个单词在组成上越接近,就有越可能拼错,P(B|A)就越大。举例来说,相差一个字母的拼法,就比相差两个字母的拼法,发生概率更高。你想拼写单词hello,那么错误拼成hallo(相差一个字母)的可能性,就比拼成haallo高(相差两个字母)。

因此

我们只要找到与输入单词在组成上最相近的那些词,再在其中挑出出现频率最高的一个,就能实现 P(B|A) * P(A) 的最大值。

正因贝叶斯公式可用于事件发生概率的推测,因此它广泛应用于计算机领域如:垃圾邮件的过滤,中文分词,机器翻译,拼写检查等等。

算法实现

  1. 建立一个足够大的文本库。读取一个包含了一百万个单词的很大的文本文件big.txt。这个文件由Project Gutenberg中几个公共领域的书串联而成。
  2. 取出文本库的每一个单词,统计它们的出现频率。
  3. 根据用户输入的单词,得到其所有可能的拼写相近的形式。
    所谓”拼写相近”,指的是两个单词之间的”编辑距离”(edit distance)不超过2。也就是说,两个词只相差1到2个字母,只通过—-删除、交换、更改和插入—-这四种操作中的一种,就可以让一个词变成另一个词。
  4. 比较所有拼写相近的词在文本库的出现频率。频率最高的那个词,就是正确的拼法。

代码实现

1、 加载Python的正则语言模块(re)和collections模块

import re, collections

2、 定义words()函数,用来取出文本库的每一个词。
正则表达式

def words(text):
    return re.findall('[a-z]+', text.lower())#将文本中的单词分离开 返回一个列表

lower()将所有词都转成小写,避免因为大小写不同,而被算作两个词,所以“the”和“The”一样定义为同一个单词。
3、定义一个train()函数,用来建立一个”字典”结构。文本库的每一个词,都是这个”字典”的键;它们所对应的值,就是这个词在文本库的出现频率。

def train(features):
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model

如果有一个单词在我们训练的数据中没有出现怎么办?最简单的方法是把这些单词看作出现了一次。这个处理叫做平滑处理,因为我们将那些概率分布可能为0的地方平滑化,将他们设置为最小的概率值1,以后每出现一次,频率就增加1。 这个类像一个所有键的值都默认为1的Python的字典(在C语言中,叫做哈希表)。
collections.defaultdict
4、使用words()和train()函数,生成上一步的”词频字典”,放入变量NWORDS。

NWORDS = train(words(open('big.txt').read()))

5、定义edits1()函数,用来生成所有与输入参数word的”编辑距离”为1的词。

alphabet = 'abcdefghijklmnopqrstuvwxyz'#字母表
def edits1(word):
   splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
   deletes = [a + b[1:] for a, b in splits if b]
   transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]
   replaces = [a + c + b[1:] for a, b in splits for c in alphabet if b]
   inserts = [a + c + b for a, b in splits for c in alphabet]
   return set(deletes + transposes + replaces + inserts)

or

def edits1(word):
    n = len(word)
    return set([word[0:i]+word[i+1:] for i in range(n)] +                     # deletion
               [word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] + # transposition
               [word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] + # replaces
               [word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])  # insertion

edit1()函数中的几个变量的含义如下:

(1)splits:将word依次按照每一位分割成前后两半。比如,'abc'会被分割成 [('', 'abc'), ('a',
'bc'), ('ab', 'c'), ('abc', '')] 。
(2)deletes:依次删除word的每一位后、所形成的所有新词。比如,'abc'对应的deletes就是 ['bc', 'ac',
'ab'] 。   
(3)transposes:依次交换word的邻近两位,所形成的所有新词。比如,'abc'对应的transposes就是
['bac', 'acb'] 。
(4)replaces:将word的每一位依次替换成其他25个字母,所形成的所有新词。比如,'abc'对应的replaces就是
['abc', 'bbc', 'cbc', ... , 'abx', ' aby', 'abz' ] ,一共包含78个词(26 × 3)。
(5)inserts:在word的邻近两位之间依次插入一个字母,所形成的所有新词。比如,'abc'
对应的inserts就是['aabc', 'babc', 'cabc', ..., 'abcx', 'abcy',
'abcz'],一共包含104个词(26 × 4)。

最后,edit1()返回deletes、transposes、replaces、inserts的合集,这就是与word”编辑距离”等于1的所有词。对于一个长度为n的单词,将会有n种删除,n-1种交换,26*n种修改,26*(n+1)种插入。会返回54n+25个词。
6、定义edit2()函数,用来生成所有与word的”编辑距离”为2的词语。

def edits2(word):
    return set(e2 for e1 in edits1(word) for e2 in edits1(e1))

但是这样的话,会返回一个 (54n+25) * (54n+25) 的数组,实在是太大了。因此,我们将edit2()改为known_edits2()函数,将返回的词限定为在文本库中出现过的词。

def known_edits2(word):
    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

7、定义correct()函数,用来从所有备选的词中,选出用户最可能想要拼写的词。

def known(words):
    return set(w for w in words if w in NWORDS)
def correct(word):
    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]
    return max(candidates, key=NWORDS.get)

max(iterable, key, default) 求迭代器的最大值,其中iterable 为迭代器,max会for i in … 遍历一遍这个迭代器,然后将迭代器的每一个返回值当做参数传给key=func 中的func(一般用lambda表达式定义) ,然后将func的执行结果传给key,然后以key为标准进行大小的判断。

完整代码

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
NWORDS = train(words(open('big.txt').read()))
alphabet = 'abcdefghijklmnopqrstuvwxyz'
def edits1(word):
   splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
   deletes = [a + b[1:] for a, b in splits if b]
   transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]
   replaces = [a + c + b[1:] for a, b in splits for c in alphabet if b]
   inserts = [a + c + b for a, b in splits for c in alphabet]
   return set(deletes + transposes + replaces + inserts)
def known_edits2(word):return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)
def known(words):return set(w for w in words if w in NWORDS)
def correct(word):
    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]
    return max(candidates, key=NWORDS.get)

缺陷

(1)文本库必须有很高的精确性,不能包含拼写错误的词。
如果用户输入一个错误的拼法,文本库恰好包含了这种拼法,它就会被当成正确的拼法。
(2)对于不包含在文本库中的新词,没有提出解决办法。
如果用户输入一个新词,这个词不在文本库之中,就会被当作正确的词。
(3)程序返回的是”编辑距离”为1的词,但某些情况下,正确的词的”编辑距离”为2。
比如,用户输入reciet,会被纠正为recite(编辑距离为1),但用户真正想要输入的词是receipt(编辑距离为2)。也就是说,”编辑距离”越短越正确的规则,并非所有情况下都成立。
(4)有些常见拼写错误的”编辑距离”大于2。
这样的错误,程序无法发现。
(5)用户输入的词的拼写正确,但是其实想输入的是另一个词。
比如,用户输入是where,这个词拼写正确,程序不会纠正。但是,用户真正想输入的其实是were,不小心多打了一个h。
(6)程序返回的是出现频率最高的词,但用户真正想输入的是另一个词。
比如,用户输入ther,程序会返回the,因为它的出现频率最高。但是,用户真正想输入的其实是their,少打了一个i。也就是说,出现频率最高的词,不一定就是用户想输入的词。
(7)某些词有不同的拼法,程序无法辨别。
比如,英国英语和美国英语的拼法不一致。英国用户输入’humur’,应该被纠正为’humour’;美国用户输入’humur’,应该被纠正为’humor’。但是,我们的程序会统一纠正为’humor’。

你可能感兴趣的:(python)