大家都知道,当我们在有道词典,搜索引擎等应用中输入一个错误单词时,它们会反馈一些备选的可能时我们想要查询的正确单词。这个功能非常的常见,但大规模实现起来其实并不容易。今天无意中想起还是因为在了解最小编辑距离时顺带整理的梳理一下,仅供大家参考。
试想一下,如果是我们自己来实现这样一个拼写检查纠正器,我们会怎样入手呢 ?显然,正确的目标词肯定与输入的词在形上是非常相似,而且是一个有意义的词,这就意味着我们需要一个很大的词典,它应该包含几乎所有的被收录的词汇来应对不同用户的不同输入需求,我们还需要一个计算词与词之间相似度的方法,常用的度量指标是‘编辑距离’,顾名思义,就是两个字符串要完全一样需要编辑的总代价。有了词典和计算编辑距离的方法之后,显然我们会得到很多候选的正确答案,针对这些答案,我们如何进行选择呢 ?这也是很重要的一方面,这里一般使用贝叶斯方法来选择条件概率最大的候选项。这样,我们就完成了整个纠错器的流程。总结一下,纠错器有以下几个要素:
1. 词典
字典必须足够大,才能应对能可能多多用户的多样输入需求。同时字典中还必须记录历史输入或从特定语料中统计每个词的出现频率。
2. 编辑距离的计算
字符间的转换有以下几种情况:
1)删除
比如,输入为:‘eait’, 正确的形式为:‘eat’
这里,两个字符之间通过删除操作即可完成转换
2)插入
比如,输入为:‘earlie’, 正确的形式为:‘earlier’
这里,在单词最后插入 r 即可。
3)替换
比如, 输入为:‘eait’, 正确的形式为:‘wait’
这里需要进行两步操作,首先将e删除,然后插入w,所以替换操作的代价是删除和插入的两倍
4)旋转
比如,输入为:‘beacuse’, 正确的形式为:‘because’
这里,将 ac 旋转180度变成ca 即可。
具体的实现见代码:
def edits1(word):
"All edits that are one edit away from `word`."
letters = 'abcdefghijklmnopqrstuvwxyz'
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
deletes = [L + R[1:] for L, R in splits if R]。### 删除操作
transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]。 ### 旋转操作
replaces = [L + c + R[1:] for L, R in splits if R for c in letters]。 ### 替换操作
inserts = [L + c + R for L, R in splits for c in letters]。 ## 插入操作
return set(deletes + transposes + replaces + inserts)
综合上面四种情况,在计算编辑距离时,删除,插入的代价设为单位1, 替换的代价设为2, 旋转相当于两次替换,所以不需要额外处理。基于此,就可以计算所有与输入字符串有较小编辑距离的正确的词。编辑距离的计算主要使用动态规划算法,具体实现代码见我的上一篇博客:
一个程序搞定最小编辑距离,最大公共子串,最大连续公共子串。
3. 条件概率的计算
这里假设输入字符串味 w, 候选集合为C, 集合中的每一个元素用ci 来表示,则我们需要求的是:argmaxci ( p(ci/w) ),即输入为W的情况下,最有可能的正确词汇。这里我们就可以子节用贝叶斯理论了。
p(ci/w)=( p(w/ci).p(ci) ) / p(w)
这里假设输入任意字符串的概率是一样的,所以p(w)就是固定,所以我们要计算argmaxci ( p(ci/w) )只需要计算p(w/ci).p(ci)即可。
而p(ci)可以用ci出现的总的频数除以语料中词频总数得到。p(w/ci)表示当用户想要输入ci,但最后真正输入的却是w的概率。针对搜索引擎的话,可以通过海量的历史记录得到;针对有道词典等一般应用来说,可以直接根据p(ci)的大小来简单判断。
这里选择norvig.com上的一个简单版本作为示例,代码如下:
import re
from collections import Counter
def words(text): return re.findall(r'\w+', text.lower())
WORDS = Counter(words(open('big.txt').read()))
def P(word, N=sum(WORDS.values())):
"Probability of `word`."
return WORDS[word] / N
def correction(word):
"Most probable spelling correction for word."
return max(candidates(word), key=P)
def candidates(word):
"Generate possible spelling corrections for word."
return (known([word]) or known(edits1(word)) or known(edits2(word)) or [word])
def known(words):
"The subset of `words` that appear in the dictionary of WORDS."
return set(w for w in words if w in WORDS)
def edits1(word):
"All edits that are one edit away from `word`."
letters = 'abcdefghijklmnopqrstuvwxyz'
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
deletes = [L + R[1:] for L, R in splits if R]
transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]
replaces = [L + c + R[1:] for L, R in splits if R for c in letters]
inserts = [L + c + R for L, R in splits for c in letters]
return set(deletes + transposes + replaces + inserts)
def edits2(word):
"All edits that are two edits away from `word`."
return (e2 for e1 in edits1(word) for e2 in edits1(e1))
现在,我们还剩最后一个问题,那就是如何去评估我们的模型呢 ?
显然,实验数据是关键,这里推荐Roger Mitton的Birkbeck spelling error corpus,这是一个专门用于拼写纠错的数据集,使用该数据集,在训练集上获得了75%,测试集上68%的准确度。
显然,拼写纠错目前来说准确率并不是非常高,说明这其中有很多其他的工作我们上可以去优化的,所以最后,我想大概聊聊拼写纠错的未来的研究重点。我认为主要可以从以下几个方面入手:
1)字典
显然,整个模型的核心是词典,如果我们的词典不够完善,那很有可能找不到用户真正想要的正确答案。所以使用更多的语料去训练一个更加强大的词典是非常有必要的。
2)候选集
目前的候选集主要是根据编辑距离计算得到的,一般考虑的是编辑距离为1或2的词,但其实很多时候目标词与输入词之间的距离大于2,所以可以将更大的编辑距离考虑在内。
3)p(w/ci)的计算
目前p(w/ci)的计算我个人认为是比较模糊的,并没有说很可靠的方式,所以这也可以作为一个切入点。
4)输入词的长度
很多针对英文的拼写检查都只是检查单个词,多个连词构成的字符串的检查还需要更多人去研究。
对该应用感兴趣的朋友推荐看下面的这些资料,写的很好。
- Roger Mitton 关于拼写检查的survey article。
- Jurafsky and Martin 在他们的文章 Speech and Language Processing也提到了拼写纠正。
- Manning and Schutze在文章 Foundations of Statistical Natural Language Processing,提到了概率语言模型,但是没有涉及拼写这方面,也可以作为参考。
- aspell 项目提供了很多可供实验用的数据,可以作为参考。
- LingPipe 项目有一个工具 spelling tutorial是关于拼写这一块的。