九、最后的大boss

提示:如果看不下去,建议学习视频课程,由于视频载体的原因,讲解更详细。

经过这么多口舌的解释,我们将上文推理的大致相等的概率约去,我们最终得到了如下的表达式:

1
2
3
4
5
P(fuke|fuk) 对应 P(fuke) 
P(fuck|fuk) 对应 P(fuck) 
P(fulk|fuk) 对应 P(fulk) 
P(furk|fuk) 对应P(furk)
P(fcuk|fuk) 对应P(fcuk)

我好像听到有同学在问?怎么得到的,好吧,我告诉你,因为,上面说了P(fuk|fuke)、P(fuk|fuck)、P(fuk|fulk)、P(fuk|furk)、P(fuk|fcuk)这几个的概率基本一样,所以,我们可以直接约去。

最后,我们比较P(fuke|fuk)、P(fuck|fuk)、P(fulk|fuk)、P(furk|fuk)、P(fcuk|fuk)这几个概率,只需要比较他们对应的概率的大小就可以了。 幸运的是,在上面我们已经说了P(fuke) 、P(fuck) 、P(fulk)、P(furk)、P(fcuk)这几个概率是能够很容易计算出来的。

十、代码详解

学习完了上面的理论,我们再次对最开始我们写的代码,做一个详细的解释。我们将代码列出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# -*- coding: utf8 -*-
 
import re, collections
 
# re是正则表达式,将text中的所有内容 变为小写,并提取出所有字母组成的单词
def words(text):
    return re.findall('[a-z]+', text.lower())
 
# 训练每个单词出现的次数,放到一个model这个字典中。
def train(features):
    # 使用defaultdict的好处在于当访问一个不存在的键值的时候会调用入参函数,
    # 并将结果作为这个key的value
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model
 
# 将big.txt中的词汇读出来,并通过words转换为一个个单词,
file_context = file('big.txt').read()
NWORDS = train(words(file_context))
 
alphabet = 'abcdefghijklmnopqrstuvwxyz'
 
#实现编辑距离为1的操作,也就是说只改变一个字母
def edits1(word):
    n = len(word)
    # 删除一个字母
    deletes = [word[0:i]+word[i+1:] for i in range(n)]
    # 移动一个字母
    transposes = [word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)]
    # 替换一个字母
    replaces = [word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet]
    # 插入一个字母
    inserts = [word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet]
    return set(deletes + transposes + replaces + inserts)
 
#实现编辑距离为2的操作,也就是说只改变两个字母,并且是已知的正确的词
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]
    # max
    return max(candidates, key=lambda w: NWORDS[w])
 
if __name__ == "__main__":
    word = "agret"
    print correct(word)
  1. 第1行,本段代码使用的是utf-8编码,也就是本代码中可以使用中文注释。如果没有第一行,你试一下,看python会不会报复你。
  2. 第3行,导入了python原生的正则表达式(re)和一个集合模块(collections)。
  3. 第6含到底48行,定义了实现拼写检查的一些关键函数,其中def是关键字,用于定义python函数。这里的代码,我们在后面会详细解释,不要着急。
  4. 第50行,变量__name__有些特别的,当我们直接执行代码所在的文件时,name的值为main,当使用import从其他文件导入该文件时,name的值是该文件的文件名,也就是其他文件导入该文件时,是不会执行51和52行的代码的。所以这里,我们直接运行该文件,那么就会执行第50、51行的代码。
  5. 第52行,调用correct函数,这个函数的参数是输入的单词,输出是朴素贝叶斯方法计算得到的词,这个词是最大可能的正确词。correct函数中调用了known函数。这个函数我们在下面解释。
  6. 剩下的就是words、train、edits1、known_edits2等几个函数了,由于他们比较费解,所以我们单独学习一下。亲爱的同学,要有耐心哦,因为我也是如此有耐心的再给大家讲解。

十一、words函数的收集单词

words函数返回一个单词的列表。参数text是一段文本数据,例如“I have a dream。”,当然实际的text可能是一本书,如《莎士比亚全集》,text包含几十上百万个单词都是有可能的。

word函数的代码如下:

1
2
3
# re是正则表达式,将text中的所有内容 变为小写,并提取出所有字母组成的单词
def words(text):
    return re.findall('[a-z]+', text.lower())

1、将所有单词转换为小写:text.lower()分析

words函数中首先将text转换为小写,使用的是text.lower()函数。这里将单词转换为小写只是为了简单,因为,拼写错误并不关心大小写,只要单词的拼写是正确的就可以了。而如果区分大小写,那么可能出现很多意义相同,但是形势不同的单词,例如A和a,love和Love,iphone和iPhone,他们虽然形态上不一样,但是表示的是同一个词。如果这也区分,那么这无疑增加了词汇的数量,给我们算法带来了一些复杂性。

所以,为了简单的给大家阐述原理,我们这里把所有的单词都转换为小写。

2、使用正则表达式,将一整本书变成单词列表

word函数中,我们重点看看这句话:

re.findall(‘[a-z]+’, text.lower())

其中text.lower上面已经说了,重点是re是什么意思。Ok,我知道可能我们的学员一点都不理解python,但是我们承诺过,不以语言作为障碍,阻碍大家步入人工智能的殿堂。所以,我们哪里不懂就会讲到哪里,希望大家不要觉得啰嗦。哈哈。

re这个正则,是需要使用import语法导入的,所以大家对照代码可以看到import re这一句代码。

现在我们对re.findall这个函数进行解释:

re.findall方法能够以列表的形式返回能匹配的子串,也就是它最终返回的是一个列表[],在其他编程语言中,叫做数组。findall的原型如下:

1
re.findall(pattern, string[, flags]):

参数pattern是需要匹配的模式,

参数string是输入的需要匹配的字符串

参数flags是一个可选参数。

熟悉了函数本身,我们看看下面代码的意思:

1
re.findall('[a-z]+', text.lower())

其实很简单,就是匹配单词的意思,所有由字母a到z组成的模式(单词),就会被提取出来,加入最终返回的列表中。所以,分析到现在,我们知道words函数是将一本书或者一段文本中出现的单词以列表的形式提取出来。注意并没有去重,也就是说,列表中会出现重复的单词,不过这正是我们想要的。

补充一下[a-z]+这个正则表达式,中括号表示里面的字母可以出现,a-z表示a到z的字母。+号表示中括号中的内容可以出现任意次。

十二、统计每个单词出现的次数:train函数解析

train函数用来统计一个列表中,出现的单词,每个单词出现的次数。为了便于分析,我们将train的代码列出如下,(电脑的好处就是可以随时copy大段代码,而不用担心浪费纸张):

1
2
3
4
5
6
7
8
# 训练每个单词出现的次数,放到一个model这个字典中。
def train(features):
    # 使用defaultdict的好处在于当访问一个不存在的键值的时候会调用入参函数,
    # 并将结果作为这个key的value
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
return model

参数features是一个列表,就是上面用words分析出的单词列表。featrue有特征的意思,其实这里理解成样本也未尝不可。

1、collections字典的的用法

Collections是一个集合模块,它包含了常用的集合类。这是我们接触的新python概念。

Collections模块中包含了一个字典类defaultdict,中文解释一下,就是“默认字典”,字典大家都理解,像我们小时候使用的《新华字典》,就是一个汉字,对应一个解释,这是简单的key-value结构。

明白了字典,那么什么是默认字典呢?其实很简单,就是字典中每一个key,当创建的时候,就对应一个默认的value值。

下面是一个在python终端中执行的代码例子:

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A')
>>> dd['key1'] = 'abc'
>>> dd['key1'] # key1存在
'abc'
>>> dd['key2'] # key2不存在,返回默认值
'N/A'

defaultdict可以使用任何不带参数的函数作为参数,函数的返回结果就是“默认字典”的默认值。

这里我们使用了一个lambada表达式,这里的lambda: 'N/A'等价于一个匿名函数,如下:

1
2
def xx(): 
    return “N/A”

lambada表达式就是用一句话代替一个函数的方法,从而简化代码,它唯一的作用就是简化代码,并没有引入什么高科技。鉴于本章的重点,我们这里就不详细解释lambada表示了。有兴趣的同学,可以自行查找资料学习一下。

2、默认字典的使用

这里使用默认字典,生成了一个所有value的默认值为1的字典。

1
model = collections.defaultdict(lambda: 1)

为什么要给model这个字典设置默认值为1,而不是0呢?我们要从model的语意来讲解一下。首先我们想统计每个单词出现的次数,所以默认值确实应该是0,但是,我们找的样本书籍,也就是我们找的数据,万一真的没有某个单词,例如没有happy,那么字典model[‘happy’]就会返回0,这就会在语义上有个小bug,happy这个词确实在显示生活中存在,但是为什么出现的次数确实0次,至少应该出现一次啊。至于在我们给出的样本书籍中没有出现,也许仅仅是写这本书的人,从来没有学过happy这个词,不代表他在其他书籍中不会出现啊。 所以,我们只能认为happy这个单词出现的概率小,而不是happy这个单词永远不会出现。出现少,为了方便,我们就用出现1次来表示吧,所以,这里model中所有元素默认的值为1。 Ok,不知道,您是否已经理解了,我想我已经说得很清楚了哦,就差像研究漂亮女人的身体一样来研究model的默认值了。如果不懂,请再看一遍上面的解释。