如何从括号中的解析树中提取派生规则?

时间:2015-06-13 08:33:16

标签: java parsing recursion nlp pseudocode

我有很多像这样的解析树:

( S ( NP-SBJ ( PRP I  )  )  ( INODE@S ( VP ( VBP have  )  ( NP ( DT a  )  ( INODE@NP ( NN savings  )  ( NN account  )  )  )  )  ( . .  )  )  )

这样的句子:“我有一个储蓄账户。”

我需要从这些树中提取所有派生规则。 推导规则如:

S -> NP-SBJ INODE@S
NP-SBJ -> PRP 
PRP -> I
INODE@S -> VP NP
and so on.

为此目的,是否有任何准备好的代码(最好是在java中)或伪代码?

修改

我认为这个问题在很多领域都非常普遍和普遍。简化的问题是从括号树中找到每个父项及其子项。

3 个答案:

答案 0 :(得分:1)

步骤1:解析括号中的字符串以创建AST

您可能没有这样想过,但字符串本身是由无上下文语法定义的:

Node :== '(' String Node* ')' |
         '(' String String ')'

我们的第一步是使用recursive descent parser语法来生成由以下类定义的抽象语法树:

class Node {
  string component;
  List<Node> children = new ArrayList<Node>();
  string word;
}

首先,标记括号中的字符串并将令牌放入队列中。我认为string.split("\\s+")应该有效,因为所有的括号和字符串都用空格分隔。

Node parse(Queue<string> tokens) throws ParseException {
  Node n = new Node();
  if (!tokens.remove().equals("(")) {
    throw new ParseException();
  }
  n.component = tokens.remove()
  if (n.component.equals("(") || n.component.equals(")")) {
    throw new ParseException();
  }
  if (tokens.element().equals("(")) {
    while (tokens.element().equals("(")) {
      Node child = parse(tokens);
      n.childen.add(child);
    }
  } else if (!tokens.element().equals(")")) {
    n.word = tokens.remove();
  } else {
    // we weren't expecting a close-paren yet
    throw new ParseException();
  }
  if (!tokens.remove.equals(")")) {
    throw new ParseException();
  }
  return n;
}

第2步:遍历AST以构建规则。

使用Ira Baxter发布的伪代码执行此步骤。

For each interior node N do:
    Use N's children C1, C2, ... Ck to generate a rule  "N = C1 C2 .. Ck". 
Eliminate duplicate rules.

出于此算法的目的,内部节点是word == nullchildren不为空的节点。 &#34;对于每个内部节点N&#34;步骤可以通过树的预订或后序遍历来执行。

因此,让我们定义一个规则类。

class Rule {
  string left;
  List<String> right = new ArrayList();

  // define the equals and hashcode methods appropriately.
  // We'll need them because we're inserting this class into
  // a HashSet.
}

让我们定义一个通用的树遍历函数

interface Visitor {
  void handle(Node n);
}

void traverse(Node n, Visitor v) {
  v.handle(n);
  for (Node child: n.children) {
    traverse(child, v);
  }
}

让我们定义构建和重复删除规则的访问者

class RuleBuilder implements Visitor {
  Set<Rule> rules = new HashSet<Rule>;

  public void handle(Node n) {
    if (n.word != null) {
      return;
    }
    Rule r = new Rule();
    r.left = n.component;
    for (Node child: n.children) {
      r.right.add(child.component);
    }
    rules.add(r);
  }
}

将它们绑在一起

Queue<string> tokens = new LinkedList(Arrays.asList(string.split("\\s+")));
Node ast = parse(tokens);
RuleBuilder ruleCollector = new RuleBuilder();
traverse(ast, ruleCollector)

您需要的规则位于ruleCollector.rules

答案 1 :(得分:0)

我为python写了这个。我相信你可以把它看成伪代码。 我稍后会编辑Java的帖子。我稍后添加了Java实现。

import re

# grammar repository 
grammar_repo = []

s = "( S ( NP-SBJ ( PRP I  )  )  ( INODE@S ( VP ( VBP have  )  ( NP ( DT a  )  ( INODE@NP ( NN savings  )  ( NN account  )  )  )  )  ( . . )  )  )"
# clean the string (make sure there is no double space in it)
s = s.replace("   ", " ").replace("  ", " ")

# find inner parenthesis (no-need-to-parse-grammar) or (lhs rhs) format
simple_grammars = re.findall("\([^\(\)]*\)", s)
# repeat until you cannot find any ( lhs rhs ) grammar
while len(simple_grammars) > 0:
    # add all simple grammar into grammar repository
    # replace them with their head
    for simple_grammar in simple_grammars:
        grammar = simple_grammar.split(" ")
        # '(' == grammar[0] and ')' == grammar[-1]
        lhs = grammar[1]
        rhs = grammar[2:-1]
        grammar_repo.append((lhs, rhs))

        s = s.replace(simple_grammar, lhs)

    simple_grammars = re.findall("\([^\(\)]*\)", s)

简而言之,从最简单的语法开始,你可以找到并用左手边替换它们并继续。例如找到(PRP I)保存,然后将其替换为PRP。重复,直到找到所有语法。

<强>更新 Java实现有点不同,但它的想法是一样的。完整代码位于:http://ideone.com/0eE8bd

PrintStream ps = new PrintStream(System.out);
ArrayList grammarRepo = new ArrayList();
String item, grammar;
String str = "( S ( NP-SBJ ( PRP I  )  )  ( INODE@S ( VP ( VBP have  )  ( NP ( DT a  )  ( INODE@NP ( NN savings  )  ( NN account  )  )  )  )  ( . . )  )  )";
// cleanup double spaces
while (str.contains("  ")){
    str = str.replaceAll("  ", " ");
}
// find no parenthesis zones!
Matcher m = Pattern.compile("\\([^\\(\\)]*\\)").matcher(str);

// loop until nothing left:
while (m.find()) {
    item = m.group();
    // make the grammar:
    grammar = item.substring(1, item.length()-1).trim().replaceFirst(" ", " -> ");

    if (!grammarRepo.contains(grammar)) {
        grammarRepo.add(grammar);
        ps.print(grammar + "\n");
    }

    str = str.replace(item, grammar.split(" -> ")[0]);
    m = Pattern.compile("\\([^\\(\\)]*\\)").matcher(str);
}

输出:

PRP -> I
NP-SBJ -> PRP
VBP -> have
DT -> a
NN -> savings
NN -> account
INODE@NP -> NN NN
NP -> DT INODE@NP
VP -> VBP NP
. -> .
INODE@S -> VP .
S -> NP-SBJ INODE@S

答案 2 :(得分:-1)

许多解析器构建的AST与语法的确切形状不匹配。

在这些情况下,应该很清楚,您无法从AST重新生成原始语法。因此,对于广泛可用的解决方案而言,这不是问题。

在极少数情况下,AST与语法完全匹配,你可以做得更好。 (您可以使用不匹配的AST来完成语法的粗略近似):

For each interior node N do:
    Use N's children C1, C2, ... Ck to generate a rule  "N = C1 C2 .. Ck". 
Eliminate duplicate rules.

这提供了语法的一个子集,它涵盖了您拥有的实例树。如果解析器未在此实例上使用语法规则,则它将不会出现在集合中。您可能需要通过此过程运行大量实例树以获得对规则的良好覆盖,但您无法确定您是否拥有完整的集合。