主要通过《自然语言处理入门》(何晗)的第2章来学习AC自动机。这里主要记录我在学习过程中整理的知识、调试的代码和心得理解,以供其他学习的朋友参考。
AC自动机是用来解决如下问题:
仅通过对文本的一次扫描,就查询出文本内包含的所有出现在词典中的词。
其目的是简化全切分扫描过程的复杂度。
下面我尽可能用通俗的语言来表达。
在全切分(查询出文本内包含的所有出现在词典中的词)长度为n的文本时,需要遍历文本中所有的连续序列。
例如,对于文本“我是文本”,就需要判断以下10个连续序列是否存在于词典中:
我、我是、我是文、我是文本、是、是文、是文本、文、文本、本
但是,这样需要在字典树中查询10次。那么,一次扫描就查询出所有在字典中出现过的词呢?这就引入了多模式匹配的问题。
给定多个词语(也称模式串),从母文本中匹配它们的问题称为多模式匹配。
AC自动机就是解决多模式匹配的方法。
AC自动机
在使用字典树扫描“我是本文”时候,在以“我”为起点扫描了“我”、“我是”、“我是文”、“我是文本”之后,又得退回到“是”,继续扫描“是”、“是文”、“是文本”……
如果能够在扫描“我是文本”的过程中,想办法知道“是文本”、“文本”、“本”在不在字典树中,就可以省略到这3次查询。这3个字符串的拥有共享递进式的后缀,首尾对调后(“本”、“本文”、“本文是”)恰好可以用另一颗前缀树索引,称它为后缀树。
而AC自动机就是在前缀树的基础上,为前缀树上的每个节点建立一棵后缀树,节省了大量查询。
简单来说,就是我们希望实现:在扫描的过程中,无论扫描失败与否,均会在自动机中继续转移,直至将文本扫描完成。
AC自动机具体包括goto表、fail表和output表。
goto表
gotu表其实就是一棵前缀树,用来将每个模式串索引到前缀树上。我们也使用《自然语言处理入门》中的ushers作为母文本,模式串集合为{he,she,his,hers}
![09dc1bbb36050da728186baba7315b94.png](https://i-blog.csdnimg.cn/blog_migrate/426c482b0a14ef68202a770d45289e8a.jpeg)
图片来自《自然语言处理入门》
值得注意的是,goto表与字典树的区别是,goto表的根节点不光可以按h和s转移,还接受任意其他字符,转移重点都是自己。这样就形成了一个圈,使得一棵树变为一幅有向有环图。
这个圈的目的在于,扫描时若遇到非h且非s字符时,状态机一直保持初始状态。这就使扫描到其他字符时一直维持在初始状态,不会失败。
有向有环图:“图论”相关知识,有回路的有向图。
AC算法是基于自动机的算法,为了区别于树结构,接下来我们按照书中的方法用“状态”来称呼节点。
output表
给定一个状态(节点),我们需要知道该状态是否对应某个或某些模式串,以决定是否输出模式串(是否存在于词典中)以及对应的值,这时我们用到的关联结构就是output表。
在ushers案例中,output表中的状态就是图中的深蓝色节点,对应的output如下:
2→he5→he,she7→his9→hers
虽然称作表,但实际上output表可以看做是状态对象的一个成员变量。如下图所示:
![b0f3a213803e085e87277040e26345c9.png](https://i-blog.csdnimg.cn/blog_migrate/72a37c7e2ca9d8ea09f684e3e7456a1d.jpeg)
图片来自《自然语言处理入门》
由图中可见,output表中的元素有两种,一种是从初始状态到当前状态的路径本身对应的模式串(例如2号),另一种是路径的后缀所对应的模式串(例如5号中的he)。
于是,output表的构造也分为两步,第一步是记录完整路径对应的模式串,第二步则是找出所有路径后缀及其模式串。其中第二步可以与fail表的构造同步进行。
之所以需要output表,是因为在AC自动机中,可能会通过fail表转移状态;因此在扫描过程中,我们也无法按从初始点的路径获取模式串,因此需将状态对应的模式串存为状态的成员变量。
fail表
在上图的表格中,我们仍然无法实现从除初始状态以外的状态,继续稳定转移状态。由此,我们引入fail表的概念。
fail表保存的是状态间一对一的关系,存储状态转移失败后应当回退的最佳状态。
最佳状态指的是能记住已匹配上的字符串的存在于goto表中的最长后缀的那个状态。
例如,匹配she后来到状态5,再来一个字符,goto失败;此时最长后缀为he,对应路径0-1-2,因此状态2为状态5 fail的最佳选择。fail到状态2之后,自动机记住了he,做好了接受r的准备。
又如,匹配his后来到状态7,再来一个字符,goto失败;此时最长后缀为is,但没有对应路径,次长后缀为s,对应路径0-3,因此状态7应当fail到状态3。
fail表的构建流程
定义S为当前状态;S.goto(C)为转移表,返回S按字符c转移的状态,null表示转移失败;S.fail为fail表,代表转移失败时候从状态S回退的状态。
- 初始状态的goto表是满的,永远不会失败,因此没有fail指针。与初始状态直接相连的所有状态,其fail指针都指向初始状态。
- 从初始状态开始进行广度优先遍历(BFS),若当前状态S接受字符c直达的状态为T,则沿着S的fail指针回溯,直到找到第一个前驱状态F,使得F.goto(c)!=null。将T的fail指针设为F.goto(c)。简单来说,就是寻找状态S的存在于goto表中的最长后缀的状态F。
- (更新Output表)由于F路径是T路径的后缀,因而T的output也应包含F的output,因此将F的output添加到T的output中。
加上完整的fail表后,自动机下图所示:
![0924d87abf76a84aa86dfea526d6c1ad.png](https://i-blog.csdnimg.cn/blog_migrate/a16aa60972c57e99868fdd5a208436ff.jpeg)
图片来自《自然语言处理入门》
基于HanLP的Python实现
from pyhanlp import JClassdef classic_demo(): words = ["hers", "his", "she", "he"] Trie = JClass('com.hankcs.hanlp.algorithm.ahocorasick.trie.Trie') # 利用JClass取得HanLP中的AC自动机 trie = Trie() for w in words: trie.addKeyword(w) # 添加模式串 for emit in trie.parseText("ushers"): # 全切分文本 print("[%d:%d]=%s" % (emit.getStart(), emit.getEnd(), emit.getKeyword()))if __name__ == '__main__': classic_demo()
运行结果
[2:3]=he[1:3]=she[2:5]=hers
学习使用教材:《自然语言处理入门》(何晗):2.6
本文中代码大部分引自该书中的代码,个人非常推荐这本书,确实是非常好的教材。