第七章主题:算法实用化——从身边的例子来看理论、研究的实践投入
本章keywords:实现关键字替换——正则(Perl),Tire,AC算法,贝叶斯过滤器实现文档分类(模式识别/机器学习),拼写错误改正功能
第19课:算法和算法评测
实用算法也就到O(n logn)附近。再高,复杂度就会随着n的增加而急剧增大,经常导致计算无法结束。
例如, 一般的基于比较的排序算法无论怎么优化,也不可能比O(n logn)快,这一点在理论上可以证明。因此,排序算法能达到O(n log),就可以认为是高速算法。
复杂度记法适用于比较算法,但在实现时不应只考虑复杂度。而且常数项经常取决于实现方法,因此实现时要尽力减小常数项。
第20课: Hatena Diary的关键字链接
关键字链接功能就是将输入的文章与27万条关键字字典进行匹配,将必要的地方替换成链接。链接替换操作实际上只是将特定关键字替换成HTML链接标签而己,所以问题就是如何对文章中的关键字进行文本替换。
随着关键字的数量不断增加,问题就来了。处理正则表达式要花费很长时间。最耗时的地方有两处:
1.编译正则表达式的处理;
2.用正则表达式进行模式匹配的处理。
对于1,可以预先创建正则表达式并保持在内存或磁盘上,即通过缓存的方法能够绕过去了。
对于问题2,把完成了关键字链接的正文文本进行缓存等处理后,开始时能绕过该问题,但要将新添的关键字反映到关键字链接中,必须花费一定时间重新建立缓存,或者从博客服务的特性上看,多一半文章的访问量并不大,导致缓存很难生效,所以该问题并没有完全解决。
用模式匹配实现关键字链接的问题
关键字链接计算耗费大量时间的原因在于正则表达式的算法。
正则表达式的模式匹配是基于自动机实现的。而且, Perl 的正则表达式实现采用的是NFA (非确定型有穷自动机)。不仅Perl ,实用的语言大多采用了NFA 引擎。像(foo|barlbaz) 这种模式匹配, NFA 会使用一种简单的方法,从输入的开头尝试匹配,失败的话就尝试下一个单词,如果又失败了,就再次尝试下一个单词。因此, foo 不匹配就尝试bar ,如果还不匹配,就尝试baz.. …·如此循环下去。因此,计算量与关键字的个数成比例。
从正则表达式到Trie——改变匹配的实现方式(空间换时间)
为解决模式匹配带来的复杂度问题,我们把实现方法从正则表达式变成了Trie 树。
Trie 的特点就是将公共前缀综合到一起,以避免浪费。
把Trie 结构当作字典进行模式匹配,其计算量要比正则表达式少得多。将输入文本输入到Trie 中,遍历它的边,如能找到终端,就可以认为该单词存在。与(foolbarlbaz)的正则表达式相比,公共前缀只需搜索一次即可。
考虑hogefoo这个单词。用该单词对包含foo 、bar、baz 的Trie 结构进行遍历。由于不包含h 字符串,所以不会匹配。接下来遍历ogefoo 、gefoo 、efoo 也一样。然后,用f 遍历Trie 时会发现,后面有oo ,因此foo 能匹配。但计算量不会超过hogefoo 的长度。
而使用正则表达式进行模式匹配时,首先要用h 与foo 、bar 、baz 等比较是否匹配,然后用o做同样的操作,如此反复,花费的时间与关键字数量呈比例。因此关键字词典越大,两者的差距就越明显。
Aho-Corasick 算法(更高速)
根据字典创建执行模式匹配的自动机,以实现在线性时间内对输入文本进行计算。这种高速算法的复杂度与字典大小无关。
Aho-Corasick 算法利用Trie 进行模式匹配,但它添加了返回的边,匹配失败时可以沿着边返回。
换成Regexp::Ljst
生成基于Trie 的正则表达式。
也就是说,该函数库并非将巨大的正则表达式按照我们最初的实现那样用OR 连接,而是用Trie 对正则表达式进行优化。利用该函数可以将
qw/foobar fooxar foozap fooza/
这个正则表达式变换成如下的正则表达式:
foo (? : [bx] ar I zap?)
比用OR 链接所有单词的尝试次数少得多。减少的原因与上述对Trie 的解释相同。
利用Regexp::List 不仅能抑制计算量,而且还能作为正则表达式使用。最初简单地采用正则表达式实现时,可以组合各种正则表达式的选工页,还能使用Perl 语言特有的各种功能,拥有丰富的灵活性。但改成Aho-Corasick 算法就失去了这个优势,也谈不上灵活性了。采用Regexp::List 可以同时拥有两者的优势。
第21课 Hatena Bookmark 的文章分类
用户向Hatena Bookmark 提交新文章后, Hatena Bookmark 系统就会通过HTTP 获取文章内容,根据文本内容进行分类,以判断其类别。
用贝叶斯过滤器判断类别
贝叶斯过滤器接收文本作为输入,并使用朴素贝叶斯( NaiveBayes) 算法从概率上判断文章属于哪个类别。其特点是判断未知文章时,要利用以前己分类数据的统计信息进行判断。必须事先由人提供"正确数据", 告诉贝叶斯过滤器哪篇文章属于哪个类别,让它"学习",最后才能自行进行正确判断。
这种事先提供学习数据,使计算机能针对未知输入进行某种计算的处理,是"机器学习"领域的研究成果。
另外,像贝叶斯过滤器这种根据已有文章一一即模式一一进行分类,属于"模式识别"领域。灵活应用机器学习、模式识别领域的算法,不仅能实现自动分类,还能开发别具一格的软件。
======
用朴素贝叶斯进行类别判断,就是给定某篇文档D,求出该文档所属概率最高的分类C的问题。也就是说,给定文档D,求出属于分类C的条件概率:
P(C|D)
设多个分类中概率最高的分类为C,即为最终选择的分类。
直接计算条件概率P(C|D)比较困难,可以利用"贝叶斯定理"将其变成可计算的公式。与其说贝叶斯定理云云,倒不如用众所周知的数学理论对概率公式变形。变形结果为
P(C|D) = P(D|C)P(C) / P(D)
只需求出右边的各个概率P(D|C)、P(C)、P(D)即可。
请注意,类别判断所需的并不是具体的概率值,只需比较各个分类,求出概率最大者即可。那么,分母P(D)为文档D条件的概率,对于所有类别而言该值都相同,比较时可以忽略。
因此只需考虑以下两个:
P(D|C)
P(C)
要判断类别,只需从学习数据的统计信息中计算出这两个值即可。求这两个值实际上非常简单。
P(C)是某个分类的出现概率,只需事先保存学习数据被保存到各个分类中的次数,即可计算。
至于P(D|C),可以把文档D看成任意单词W连续出现的结果,那么P(D|C)可以近似成P(W1|C) P(W2|C) P(W3|C)…P(Wn|C)。这样只需将文档D分割成单词,求出每个单词被分类到各个类别中的次数,即可求出P(D|C)的近似值。
======
最后结论是,只需给出正确数据,并保存正确数据被使用的次数,以及各个单词的出现次数,然后通过朴素贝叶斯算法计算概率,就能判断出类别。其他数据可以全部抛弃。
下面以Hatena Bookmark为例,简单列举一下将贝叶斯过滤器实现的类别分类引擎融入产品所需的其他工作。
分类引擎是用C++开发的。需要将引擎变成网络服务器。
编写Perl客户端,与该服务器通信并获取结果。由Web应用程序调用。
为了定期备份学习数据,要给C++引擎添加数据转储和加载功能。
人工准备1000条学习数据。这是必须由人努力完成的……
实现统计,以跟踪判断精度是否足够。将统计画成图表,以进行调优。
考虑冗余化,建立standby系统。自动切换功能会消耗很多工时,因此只需能从备份系统中加载数据就可以了。
在Web应用程序上准备用户界面。
拼写错误改正功能的制作方法--Hatena Bookmark的搜索功能
基本流程:
1正确数据采用Hatena Keyword的字典,它拥有27万条正确数据
2计算用户输入的搜索查询与字典中的语句之间的编辑距离,定量衡量错误程度
3以一定的错误程度为基准,从字典中找出某个单词群作为候补正确答案
4将3的候补正确答案以Hatena Bookmark的文章中的单词使用频率为基准,按照正确的可能性排列
5将使用频率最高的单词作为正确答案,提示给用户
这个拼写错误改正程序必须知道什么是正确答案。正确数据采用了Hatena Keyword。如果使用仅包含地名的字典,就变成了地名改正引擎,使用仅包含餐厅名称的字典就成了餐饮改正引擎,很有意思。
如果无法自己准备通用字典,可以下载Wikipedia等的数据,用它包含的单词作为字典也可以。
2 计算搜索查询与字典中语句的编辑距离,定量衡量错误程度
所谓编辑距离,就是把一个单词变成另外一个单词所需的编辑(插入、替换、删除)次数,用它可以定量衡量单词之间的距离。
看个例子更容易理解。
(伊藤直哉,伊藤直也)→1
(伊藤直,伊藤直也)→1
(佐藤直哉,伊藤直也)→2
(佐藤B作,伊藤直也)→3
各个组合的编辑距离如上所示。可见,编辑距离为1的单词相似程度的确比编辑距离为3的高。
众所周知,编辑距离可以用动态规划算法简单、高速地实现,是动态规划解决问题的代表例。Hatena Bookmark采用的是由普通的编辑距离--Levenshtein距离--发展而来的Jaro-Winkler距离。Jaro-Winkler距离这种定量方法对于靠前的错误单词有较高的惩罚,这是因为姓名中姓氏经常出错,而名字却不容易写错。Jaro-Winkler距离就由这种直感而来。
3 以一定的错误程度为基准,从字典中找出某个单词群作为候补正确答案
这样,比较输入查询与字典中的单词的编辑距离,获得编辑距离较小的单词一览。但是,字典中有27万条单词,应当避免全部比较。
我们采用的数据结构,可以先创建了字典的n-gram索引,仅仅取出与输入语句的bi-gram重叠度高的单词。看看图F.2就很容易理解。
用这种数据结构,可以预先缩小比较对象,之后再逐个计算编辑距离。
4 将候补正确答案以文章中的单词使用频率为基准,按照正确的可能性排列
然后将计算出的编辑距离较小的作为候补答案,那么编辑距离都是1、2这种跳跃的值,因此经常会得到多个候补。输入伊藤直弥,就会得到这些正确答案候补:
伊藤直也
伊藤直哉
伊东直也
哪个才是正确答案呢?
一个方法是选择搜索空间中出现频率最高的单词作为正确答案。Hatena Bookmark就是这样做的。我们采用文档频率(Document Frequency)作为"出现频率最高"的标准。文档频率就是特定单词在Hatena Bookmark中出现在多少篇文章中的次数。Hatena Bookmark的其他功能也用到这个数值,因此该值被保存了下来,用它作为判断依据。搜索伊藤直哉而不是伊藤直也的人当然存在,但多数情况下这种方法是好用的,这就是所谓的启发式(heuristic)吧。
5 将使用频率最高的单词作为正确答案提示给用户
通过上述流程就能找到可能正确的单词,将其作为修正的候补答案提示给用户。实际上是将Jaro-Winkler 距离和文档频率相乘作为分数,然后显示分数大于一定标准的答案。这样,那些不太像是正确答案的就不会被提示了。