【问题标题】:Generating correct phrases from PEG grammars从 PEG 语法生成正确的短语
【发布时间】:2020-01-24 00:11:19
【问题描述】:

我写了一个 PEG 解析器生成器只是为了好玩(我会在 NPM 上发布它),并认为在它上面添加一个随机短语生成器会很容易。这个想法是在给定语法的情况下自动获得正确的短语。所以我设置了以下规则来从每种类型的解析器生成字符串:

  • 序列p1 p2 ... pn:为每个子解析器生成一个短语并返回连接。
  • 替代p1 | p2 | ... | pn:随机选择一个子解析器并用它生成一个短语。
  • 重复p{n, m}:在[n, m]中选择一个数字x(或[n, n+2]m === Infinity)并返回xp生成的短语的串联。
  • 终端:只返回终端文字。

当我采用以下语法时:

S: NP VP
PP: P NP
NP: Det N | Det N PP | 'I'
VP: V NP | VP PP
V: 'shot' | 'killed' | 'wounded'
Det: 'an' | 'my' 
N: 'elephant' | 'pajamas' | 'cat' | 'dog'
P: 'in' | 'outside'

效果很好。一些例子:

my pajamas killed my elephant
an pajamas wounded my pajamas in my pajamas
an dog in I wounded my cat in I outside my elephant in my elephant in an pajamas outside an cat
I wounded my pajamas in my dog

这个语法有一个递归(PP: P NP > NP: Det N PP)。当我采用其他递归语法时,这次是数学表达式:

expr: term (('+' | '-') term)*
term: fact (('*' | '/') fact)*
fact: '1' | '(' expr ')'

几乎有两次,我收到 “超出最大调用堆栈大小” 错误(在 NodeJS 中)。另一半的时间,我得到了正确的表达:

( 1 ) * 1 + 1
( ( 1 ) / ( 1 + 1 ) - ( 1 / ( 1 * 1 ) ) / ( 1 / 1 - 1 ) ) * 1
( ( ( 1 ) ) )
1
1 / 1

我猜fact 的递归产生式被调用太频繁了,在调用堆栈中太深了,这让整个事情都崩溃了。

我怎样才能让我的方法不那么天真,以避免那些爆炸调用堆栈的情况?谢谢。

【问题讨论】:

  • 请注意,使用 PEG,“替代...:随机选择一个子解析器并用它生成一个短语”不一定会产生有效的输入。这是因为 PEG 替代品是有序的,早期替代品可能会遮蔽后来替代品的前缀子集。换句话说,您可以随机选择由子解析器生成的短语,该短语以可能由早期子解析器生成的内容开头,因此解析器永远无法识别。我认为重复存在相关问题。
  • @rici 是的,有没有办法解决这个问题?
  • 简单的方法是在返回之前验证生成的句子是否可以被解析。根据您的语法,这可能需要也可能不需要大量重试;我什至不会冒险猜测。除此之外,我不知道;在我看来,几乎所有关于 PEG 的有趣理论问题都被证明是难以解决的,而我从未努力克服这一点。

标签: algorithm parsing peg


【解决方案1】:

当然,如果一个文法描述了任意长的输入,你很容易陷入非常深的递归。避免这种陷阱的一个简单方法是保留部分扩展的句子形式的优先级队列,其中关键是长度。删除最短的并以各种可能的方式替换每个非终端,发出现在都是终端的那些,并将其余的添加回队列中。您可能还想维护一个“已经发出”的集合以避免发出重复项。如果语法没有像 epsilon 产生式那样的句子形式派生出较短的字符串,那么此方法会以非递减长度顺序生成语法描述的所有字符串。也就是说,一旦你看到一个长度为 N 的输出,所有长度为 N-1 或更短的字符串都已经出现了。

由于OP询问了细节,这里是表达式语法的实现。它通过将 PEG 重写为 CFG 来简化。

import heapq

def run():
  g = {
    '<expr>': [
      ['<term>'],
      ['<term>', '+', '<expr>'],
      ['<term>', '-', '<expr>'],
    ],
    '<term>': [
      ['<fact>'],
      ['<fact>', '*', '<term>'],
      ['<fact>', '/', '<term>'],
    ],
    '<fact>': [
      ['1'],
      ['(', '<expr>', ')']
    ],
  }
  gen(g)

def is_terminal(s):
  for sym in s:
    if sym.startswith('<'):
      return False;
  return True;

def gen(g, lim = 10000):
  q = [(1, ['<expr>'])]
  n = 0;
  while n < lim:
    _, s = heapq.heappop(q)
    # print("pop: " + ''.join(s))
    a = []
    b = s.copy()
    while b:
      sym = b.pop(0)
      if sym.startswith('<'):
        for rhs in g[sym]:
          s_new = a.copy()
          s_new.extend(rhs)
          s_new.extend(b)
          if is_terminal(s_new):
            print(''.join(s_new))
            n += 1
          else:
            # print("push: " + ''.join(s_new))
            heapq.heappush(q, (len(s_new), s_new))
        break # only generate leftmost derivations
      a.append(sym)

run()

取消注释额外的print()s 以查看堆活动。一些示例输出:

1
(1)
1*1
1/1
1+1
1-1
((1))
(1*1)
(1/1)
(1)*1
(1)+1
(1)-1
(1)/1
(1+1)
(1-1)
1*(1)
1*1*1
1*1/1
1+(1)
1+1*1
1+1/1
1+1+1
1+1-1
1-(1)
1-1*1
1-1/1
1-1+1
1-1-1
1/(1)
1/1*1
1/1/1
1*1+1
1*1-1
1/1+1
1/1-1
(((1)))
((1*1))
((1/1))
((1))*1
((1))+1
((1))-1
((1))/1
((1)*1)
((1)+1)
((1)-1)
((1)/1)
((1+1))
((1-1))
(1)*(1)
(1)*1*1
(1)*1/1
(1)+(1)
(1)+1*1

【讨论】:

  • 你有更多关于这项技术的细节吗,比如论文?有名字吗?
  • 没有。但我过去曾为 CFG 实现过它来生成测试数据。它工作正常。如果您真的需要代码,我可能会编写一个示例。
  • @Strebler 好的,我添加了一个快速而简单的实现。很有趣!
  • 在生成 all 句子方​​面做得很好,但它可能需要一段时间才能生成一个有趣的中等长度的句子(这是OP 有,样本以长句为主)。请参阅 this answer 以获取一篇论文的参考,该论文提供了一种从(无 epsilon 的)CFG 生成随机句子的算法。
  • @Strebler:是的,我引用的论文中的算法有点相似,但它可以计算更准确的权重,这样你就可以真正得到一个统一的样本(给定大小的)。在 Python 中,它很容易实现,因为您不必担心算术溢出。
猜你喜欢
  • 2012-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多