自然语言处理——第三章习题——程序模拟确定性PDA

转自:https://www.nosuchfield.com/2017/01/07/Pushdown-auto

御坂研究所

1. 下推自动机

自带栈的有限状态机叫作下推自动机(PushDown Automaton,PDA)。

上面给出了下推自动机的概念,那么为什么我们要在有限自动机的基础上给其增加一个栈来构建下推自动机呢?其根本原因在于有限状态机本身无法存储数据,而加上了栈之后的下推自动机则具有了数据存储能力。具有了存储能力之后,下推自动机的计算能力相较于有限自动机得到了进一步的增强。

我们已经知道,有限自动机在进行状态转移的时候,需要读取一个输入,然后根据当前状态已经输入来进行状态转移。下推自动机在进行状态转移的时候同样要依赖于当前状态和输入,不过,因为下推自动机有了栈的概念,所以在进行状态转移的时候我们还需要一点别的东西——读栈和写栈操作。例如下图这样的一个下推自动机:

自然语言处理——第三章习题——程序模拟确定性PDA_第1张图片uploading.4e448015.gif正在上传…重新上传取消自然语言处理——第三章习题——程序模拟确定性PDA_第2张图片uploading.4e448015.gif转存失败重新上传取消自然语言处理——第三章习题——程序模拟确定性PDA_第3张图片uploading.4e448015.gif正在上传…重新上传取消自然语言处理——第三章习题——程序模拟确定性PDA_第4张图片

我们可以把这个下推自动机用这样的转移列表列出来,注意这里的 $ 符号,因为栈为空的状态并不是很好判断,所以我们添加了这个字符来代表栈为空,也就是说栈的最底部永远应该保持有 $ 这个字符。最下面的虚线代表着当栈为空且状态为2时(我们使用虚线来代表不需要有输入的转移情形,即自由移动),此时不需要任何输入,栈会自动的弹出 $ 然后再压入 $,随后状态从2变为1。

当前状态 输入字符 转移状态 栈顶字符(读入/弹出) 压入字符
1 ( 2 $ b$
2 ( 2 b bb
2 ) 2 b 不压入
2 无输入 1 $ $

相对于有限自动机,下推自动机只是增加了一个入栈和出栈的操作,可以说并不复杂,但是需要提醒的一点是:对于下推自动机,栈顶的元素也是转移的规则之一。怎么理解这句话呢?下面我通过一个小例子来说明。

有如下两个转移规则

当前状态 输入字符 转移状态 栈顶字符(读入/弹出) 压入字符
1 a 2 x $
1 a 3 y $

当在状态1读到输入a的时候,此时并不能决定到底要转移到那一个状态,而是要根据此时的栈顶元素(x或是y)来判断要进行哪一种的转移操作,所以说栈顶元素也是转移的规则之一。《计算的本质》中是这样描述这个特性的:

在某种程度上,下推自动机还能控制自己。在规则和栈之间有一个反馈环——栈的内容影响机器应该遵守的规则,而按照某个规则执行也会影响栈的内容——这允许PDA在栈中存储一些信息,这些信息可以影响它将来的执行。

因为下推自动机是有限自动机的扩充,所以下推自动机也分为确定性下推自动机和非确定性下推自动机,下面将会详细介绍。

2. 确定性下推自动机

什么是确定性?即 无冲突无遗漏

  • 对于无冲突,我们只要保证下推自动机在进行状态转移的时候不会产生模棱两可的规则即可;
  • 而无遗漏这个要求操作起来就比较的棘手,因为下推自动机一般很难把所有的转移规则都覆盖到,所以一般的做法是忽略这个约束并允许DPDA(确定性下推自动机,Deterministic PushDown Automaton)只定义完成工作所需的规则,并且假定一台DPDA在没有规则可用时将进入停滞状态。

了解DPDA的定义后,就开始使用Python来模拟一个下推自动机吧,代码实现如下:

"""
确定性下推自动机,Deterministic PushDown Automaton
"""


# 定义一个栈
class Stack(object):
    # 初始时栈底的元素
    def __init__(self, init):
        self.__storage = init

    def top(self):
        return self.__storage[-1]

    def push(self, p):
        # 压入的内容做遍历,靠近后面的内容应该先被压入
        for i in reversed(p):
            self.__storage.append(i)

    def pop(self):
        return self.__storage.pop()


# 名词[配置]表示一个状态和一个栈的组合,其实上相当于以前的[一个状态]
# 为什么要定义[配置]呢?目的在于把状态和栈这两种元素组合起来形成一个整体,方便使用
class PDAConfiguration(object):
    def __init__(self, state, stack):
        self.state = state
        self.stack = stack


# DDPA的转移规则
class PDARule(object):
    # 参数:当前状态,输入,下一个状态,(栈)弹出字符,压入字符
    def __init__(self, state, character, next_state, pop_character, push_characters):
        self.state = state
        self.character = character
        self.next_state = next_state
        self.pop_character = pop_character
        self.push_characters = push_characters

    # 下一个配置:1. 下一个状态就是 next_state 参数,2. 下一个配置的栈由方法 next_stack 根据当前的栈信息算出
    def follow(self, configuration):
        return PDAConfiguration(self.next_state, self.__next_stack(configuration.stack))

    # 判断指定配置执行指定输入时是否可用当前的转移规则
    def applies_to(self, configuration, character):
        return self.state == configuration.state \
               and self.pop_character == configuration.stack.top() and self.character == character

    # 下一个栈的计算,先弹出再压入即可
    def __next_stack(self, stack):
        stack.pop()  # 弹出
        stack.push(self.push_characters)  # 压入
        return stack


# 转移规则集合
class DPDARulebook(object):
    def __init__(self, rule_set):
        self.ruleSet = rule_set

    # 用于获取下一个配置
    def next_configuration(self, configuration, character):
        return self.__rule_for(configuration, character).follow(configuration)

    # 根据当前的配置和输入来查找对应的转移规则
    def __rule_for(self, configuration, character):
        for rule in self.ruleSet:
            if rule.applies_to(configuration, character):
                return rule
        raise Exception("找不到可供使用的规则 ...")


rulebook = DPDARulebook([
    PDARule(1, '(', 2, '$', ['b', '$']),
    PDARule(2, '(', 2, 'b', ['b', 'b']),
    PDARule(2, ')', 2, 'b', []),
    PDARule(2, None, 1, '$', ['$'])
])


class DPDA(object):
    # 参数:初始配置,接受状态,规则集合
    def __init__(self, current_configuration, accept_states, rule_book):
        self.current_configuration = current_configuration
        self.accept_states = accept_states
        self.rule_book = rule_book

    # 判断当前的状态是否是接受状态
    def accepting(self):
        return self.current_configuration.state in self.accept_states

    # 输入
    def read_character(self, character):
        self.current_configuration = self.rule_book.next_configuration(self.current_configuration, character)

    # 同样为了简化操作,方便连续输入
    def read_string(self, string):
        for c in string:
            self.current_configuration = self.rule_book.next_configuration(self.current_configuration, c)


if __name__ == '__main__':
    dpda = DPDA(PDAConfiguration(1, Stack(['$'])), [1], rulebook)
    print(dpda.accepting())
    dpda.read_string('(()')
    print(dpda.accepting())
  1. 下推自动机是有限自动机增加了一个栈
  2. 下推自动机的转移规则还需要指定栈顶元素,栈顶元素不一样的不能算为同样的转移规则

你可能感兴趣的:(自然语言处理)