仅用500行Python代码实现一个英文解析器的教程

语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理。自然语言引入了很多意外的歧义,以我们对世界的了解可以迅速地发现这些歧义。举一个我很喜欢的例子:

正确的解析是连接“with”和“pizza”,而错误的解析将“with”和“eat”联系在了一起:

过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展。现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色。

文章剩下的部分首先设置了问题,接着带你了解为此准备的简洁实现。parser.py 代码中的前 200 行描述了词性的标注者和学习者(这里)。除非你非常熟悉 NLP 方向的研究,否则在研究这篇文章之前至少应该略读。

Cython 系统和 Redshift 是为我目前的研究而写的。和麦考瑞大学的合同到期后,我计划六月份对它进行改进,用于一般用途。目前的版本托管在 GitHub 上。
问题描述

在你的手机中输入这样一条指令是非常友善的:

Set volume to zero when I'm in a meeting, unless John's school calls.

接着进行适当的策略配置。在 Android 系统上,你可以应用 Tasker 做这样的事情,而 NL 接口会更好一些。接收可以编辑的语义表示,你就能了解到它认为你表达的意思,并且可以修正他的想法,这样是特别友善的。

这项工作有很多问题需要解决,但一些种类的句法形态绝对是必要的。我们需要知道:

Unless John's school calls, when I'm in a meeting, set volume to zero

是解析指令的又一种方式,而

Unless John's school, call when I'm in a meeting

表达了完全不同的意思。

依赖解析器返回一个单词与单词间的关系图,使推理变得更容易。关系图是树形结构,有向边,每个节点(单词)有且仅有一个入弧(头部依赖)。

用法示例:

>>> parser = parser.Parser()
>>> tokens = "Set the volume to zero when I 'm in a meeting unless John 's school calls".split()
>>> tags, heads = parser.parse(tokens)
>>> heads
[-1, 2, 0, 0, 3, 0, 7, 5, 7, 10, 8, 0, 13, 15, 15, 11]
>>> for i, h in enumerate(heads):
...  head = tokens[heads[h]] if h >= 1 else 'None'
...  print(tokens[i] + ' <-- ' + head])
Set <-- None
the <-- volume
volume <-- Set
to <-- Set
zero <-- to
when <-- Set
I <-- 'm
'm <-- when
in <-- 'm
a <-- meeting
meeting <-- in
unless <-- Set
John <-- 's
's  <-- calls
school <-- calls
calls <-- unless

一种观点是通过语法分析进行推导比字符串应该稍稍容易一些。语义分析映射有望比字面意义映射更简单。

这个问题最让人困惑的是正确性是由惯例,即注释指南决定的。如果你没有阅读指南并且不是一个语言学家,就不能判断解析是否正确,这使整个任务显得奇怪和虚假。

例如,在上面的解析中存在一个错误:根据 Stanford 的注释指南规定,“John's school calls” 存在结构错误。而句子这部分的结构是指导注释器如何解析一个类似于“John's school clothes”的例子。

这一点值得深入考虑。理论上讲,我们已经制定了准则,所以“正确”的解析应该相反。如果我们违反约定,有充分的理由相信解析任务会变得更加困难,因为任务和其他语>法的一致性会降低。【2】但是我们可以测试经验,并且我们很高兴通过反转策略获得优势。

我们确实需要惯例中的差异——我们不希望接收相同的结构,否则结果不会很有用。注释指南在哪些区别使下游应用有效和哪些解析器可以轻松预测之间取得平衡。
映射树

在决定构建什么样子的关系图时,我们可以进行一项特别有效的简化:对将要处理的关系图结构进行限制。它不仅在易学性方面有优势,在加深算法理解方面也有作用。大部分的>英文解析工作中,我们遵循约束的依赖关系图就是映射树:

    树。除了根外,每个单词都有一个弧头。
    映射关系。针对每对依赖关系 (a1, a2)和 (b1, b2),如果 a1 < b2, 那么 a2 >= b2。换句话说,依赖关系不能交叉。不可能存在一对 a1 b1 a2 b2 或者 b1 a1 b2 a2 形式的依赖关系。

在解析非映射树方面有丰富的文献,解析无环有向图方面的文献相对而言少一些。我将要阐述的解析算法用于映射树领域。
贪婪的基于转换的解析

我们的语法分析器以字符串符号列表作为输入,输出代表关系图中边的弧头索引列表。如果第 i 个弧头元素是 j, 依赖关系包括一条边 (j, i)。基于转换的语法分析器>是有限状态转换器;它将 N 个单词的数组映射到 N 个弧头索引的输出数组。

弧头数组表示了 MSNBC 的弧头:MSNBC 的单词索引是1,reported 的单词索引是2, head[1] == 2。你应该已经发现为什么树形结构如此方便——如果我们输出一个 DAG 结构,这种结构中的单词可能包含多个弧头,树形结构将不再工作。

虽然 heads 可以表示为一个数组,我们确实喜欢保持一定的替代方式来访问解析,以方便高效的提取特征。Parse 类就是这样:

class Parse(object):
  def __init__(self, n):
    self.n = n
    self.heads = [None] * (n-1)
    self.lefts = []
    self.rights = []
    for i in range(n+1):
      self.lefts.append(DefaultList(0))
      self.rights.append(DefaultList(0))

  def add_arc(self, head, child):
    self.heads[child] = head
    if child < head:
      self.lefts[head].append(child)
    else:
      self.rights[head].append(child)

和语法解析一样,我们也需要跟踪句子中的位置。我们通过在 words 数组中置入一个索引和引入栈机制实现,栈中可以压入单词,设置单词的弧头时,弹出单词。所以我们的状态数据结构是基础。

  • 一个索引 i, 活动于符号列表中
  • 到现在为止语法解析器中的加入的依赖关系
  • 一个包含索引 i 之前产生的单词的栈,我们已为这些单词声明了弧头。

解析过程的每一步都应用了三种操作之一:

SHIFT = 0; RIGHT = 1; LEFT = 2
MOVES = [SHIFT, RIGHT, LEFT]

def transition(move, i, stack, parse):
  global SHIFT, RIGHT, LEFT
  if move == SHIFT:
    stack.append(i)
    return i + 1
  elif move == RIGHT:
    parse.add_arc(stack[-2], stack.pop())
    return i
  elif move == LEFT:
    parse.add_arc(i, stack.pop())
    return i
  raise GrammarError("Unknown move: %d" % move)

LEFT 和 RIGHT 操作添加依赖关系并弹栈,而 SHIFT 压栈并增加缓存中 i 值。

因此,语法解析器以一个空栈开始,缓存索引为0,没有依赖关系记录。选择一个有效的操作,应用到当前状态。继续选择操作并应用直到栈为空且缓存索引到达输入数组的终点。(没有逐步跟踪是很难理解这种算法的。尝试准备一个句子,画出映射解析树,接着通过选择正确的转换序列遍历完解析树。)

下面是代码中的解析循环:

class Parser(object):
  ...
  def parse(self, words):
    tags = self.tagger(words)
    n = len(words)
    idx = 1
    stack = [0]
    deps = Parse(n)
    while stack or idx < n:
      features = extract_features(words, tags, idx, n, stack, deps)
      scores = self.model.score(features)
      valid_moves = get_valid_moves(i, n, len(stack))
      next_move = max(valid_moves, key=lambda move: scores[move])
      idx = transition(next_move, idx, stack, parse)
    return tags, parse

def get_valid_moves(i, n, stack_depth):
  moves = []
  if i < n:
    moves.append(SHIFT)
  if stack_depth >= 2:
    moves.append(RIGHT)
  if stack_depth >= 1:
    moves.append(LEFT)
  return moves

我们以标记的句子开始,进行状态初始化。然后将状态映射到一个采用线性模型评分的特征集合。接着寻找得分最高的有效操作,应用到状态中。

这里的评分模型和词性标注中的一样工作。如果对提取特征和使用线性模型评分的观点感到困惑,你应该复习这篇文章。下面是评分模型如何工作的提示:

class Perceptron(object)
  ...
  def score(self, features):
    all_weights = self.weights
    scores = dict((clas, 0) for clas in self.classes)
    for feat, value in features.items():
      if value == 0:
        continue
      if feat not in all_weights:
        continue
      weights = all_weights[feat]
      for clas, weight in weights.items():
        scores[clas] += value * weight
    return scores

这里仅仅对每个特征的类权重求和。这通常被表示为一个点积,然而我发现处理很多类时就不太适合了。

定向解析器(RedShift)遍历多个候选元素,但最终只会选择最好的一个。我们将关注效率和简便而忽略其准确性。我们只进行了单一的分析。我们的搜索策略将是完全贪婪的,就像词性标记一样。我们将锁定在选择的每一步。

如果认真阅读了词性标记,你可能会发现下面的相似性。我们所做的是将解析问题映射到一个使用“扁平化”解决的序列标记问题,或者非结构化的学习算法(通过贪婪搜索)。
特征集

特征提取代码总是很丑陋。语法分析器的特征指的是上下文中的一些标识。

  • 缓存中的前三个单词 (n0, n1, n2)
  • 堆栈中的栈顶的三个单词 (s0, s1, s2)
  • s0 最左边的两个孩子 (s0b1, s0b2);
  • s0 最右边的两个孩子 (s0f1, s0f2);
  • n0 最左边的两个孩子 (n0b1, n0b2);

我们指出了上述12个标识的单词表,词性标注,和标识关联的左右孩子数目。

因为使用的是线性模型,特征指的是原子属性组成的三元组。

def extract_features(words, tags, n0, n, stack, parse):
  def get_stack_context(depth, stack, data):
    if depth >;= 3:
      return data[stack[-1]], data[stack[-2]], data[stack[-3]]
    elif depth >= 2:
      return data[stack[-1]], data[stack[-2]], ''
    elif depth == 1:
      return data[stack[-1]], '', ''
    else:
      return '', '', ''

  def get_buffer_context(i, n, data):
    if i + 1 >= n:
      return data[i], '', ''
    elif i + 2 >= n:
      return data[i], data[i + 1], ''
    else:
      return data[i], data[i + 1], data[i + 2]

  def get_parse_context(word, deps, data):
    if word == -1:
      return 0, '', ''
    deps = deps[word]
    valency = len(deps)
    if not valency:
      return 0, '', ''
    elif valency == 1:
      return 1, data[deps[-1]], ''
    else:
      return valency, data[deps[-1]], data[deps[-2]]

  features = {}
  # Set up the context pieces --- the word, W, and tag, T, of:
  # S0-2: Top three words on the stack
  # N0-2: First three words of the buffer
  # n0b1, n0b2: Two leftmost children of the first word of the buffer
  # s0b1, s0b2: Two leftmost children of the top word of the stack
  # s0f1, s0f2: Two rightmost children of the top word of the stack

  depth = len(stack)
  s0 = stack[-1] if depth else -1

  Ws0, Ws1, Ws2 = get_stack_context(depth, stack, words)
  Ts0, Ts1, Ts2 = get_stack_context(depth, stack, tags)

  Wn0, Wn1, Wn2 = get_buffer_context(n0, n, words)
  Tn0, Tn1, Tn2 = get_buffer_context(n0, n, tags)

  Vn0b, Wn0b1, Wn0b2 = get_parse_context(n0, parse.lefts, words)
  Vn0b, Tn0b1, Tn0b2 = get_parse_context(n0, parse.lefts, tags)

  Vn0f, Wn0f1, Wn0f2 = get_parse_context(n0, parse.rights, words)
  _, Tn0f1, Tn0f2 = get_parse_context(n0, parse.rights, tags)

  Vs0b, Ws0b1, Ws0b2 = get_parse_context(s0, parse.lefts, words)
  _, Ts0b1, Ts0b2 = get_parse_context(s0, parse.lefts, tags)

  Vs0f, Ws0f1, Ws0f2 = get_parse_context(s0, parse.rights, words)
  _, Ts0f1, Ts0f2 = get_parse_context(s0, parse.rights, tags)

  # Cap numeric features at 5?
  # String-distance
  Ds0n0 = min((n0 - s0, 5)) if s0 != 0 else 0

  features['bias'] = 1
  # Add word and tag unigrams
  for w in (Wn0, Wn1, Wn2, Ws0, Ws1, Ws2, Wn0b1, Wn0b2, Ws0b1, Ws0b2, Ws0f1, Ws0f2):
    if w:
      features['w=%s' % w] = 1
  for t in (Tn0, Tn1, Tn2, Ts0, Ts1, Ts2, Tn0b1, Tn0b2, Ts0b1, Ts0b2, Ts0f1, Ts0f2):
    if t:
      features['t=%s' % t] = 1

  # Add word/tag pairs
  for i, (w, t) in enumerate(((Wn0, Tn0), (Wn1, Tn1), (Wn2, Tn2), (Ws0, Ts0))):
    if w or t:
      features['%d w=%s, t=%s' % (i, w, t)] = 1

  # Add some bigrams
  features['s0w=%s, n0w=%s' % (Ws0, Wn0)] = 1
  features['wn0tn0-ws0 %s/%s %s' % (Wn0, Tn0, Ws0)] = 1
  features['wn0tn0-ts0 %s/%s %s' % (Wn0, Tn0, Ts0)] = 1
  features['ws0ts0-wn0 %s/%s %s' % (Ws0, Ts0, Wn0)] = 1
  features['ws0-ts0 tn0 %s/%s %s' % (Ws0, Ts0, Tn0)] = 1
  features['wt-wt %s/%s %s/%s' % (Ws0, Ts0, Wn0, Tn0)] = 1
  features['tt s0=%s n0=%s' % (Ts0, Tn0)] = 1
  features['tt n0=%s n1=%s' % (Tn0, Tn1)] = 1

  # Add some tag trigrams
  trigrams = ((Tn0, Tn1, Tn2), (Ts0, Tn0, Tn1), (Ts0, Ts1, Tn0),
        (Ts0, Ts0f1, Tn0), (Ts0, Ts0f1, Tn0), (Ts0, Tn0, Tn0b1),
        (Ts0, Ts0b1, Ts0b2), (Ts0, Ts0f1, Ts0f2), (Tn0, Tn0b1, Tn0b2),
        (Ts0, Ts1, Ts1))
  for i, (t1, t2, t3) in enumerate(trigrams):
    if t1 or t2 or t3:
      features['ttt-%d %s %s %s' % (i, t1, t2, t3)] = 1

  # Add some valency and distance features
  vw = ((Ws0, Vs0f), (Ws0, Vs0b), (Wn0, Vn0b))
  vt = ((Ts0, Vs0f), (Ts0, Vs0b), (Tn0, Vn0b))
  d = ((Ws0, Ds0n0), (Wn0, Ds0n0), (Ts0, Ds0n0), (Tn0, Ds0n0),
    ('t' + Tn0+Ts0, Ds0n0), ('w' + Wn0+Ws0, Ds0n0))
  for i, (w_t, v_d) in enumerate(vw + vt + d):
    if w_t or v_d:
      features['val/d-%d %s %d' % (i, w_t, v_d)] = 1
  return features

训练

学习权重和词性标注使用了相同的算法,即平均感知器算法。它的主要优势是,它是一个在线学习算法:例子一个接一个流入,我们进行预测,检查真实答案,如果预测错误则调整意见(权重)。

循环训练看起来是这样的:

class Parser(object):
  ...
  def train_one(self, itn, words, gold_tags, gold_heads):
    n = len(words)
    i = 2; stack = [1]; parse = Parse(n)
    tags = self.tagger.tag(words)
    while stack or (i + 1) < n:
      features = extract_features(words, tags, i, n, stack, parse)
      scores = self.model.score(features)
      valid_moves = get_valid_moves(i, n, len(stack))
      guess = max(valid_moves, key=lambda move: scores[move])
      gold_moves = get_gold_moves(i, n, stack, parse.heads, gold_heads)
      best = max(gold_moves, key=lambda move: scores[move])
    self.model.update(best, guess, features)
    i = transition(guess, i, stack, parse)
  # Return number correct
  return len([i for i in range(n-1) if parse.heads[i] == gold_heads[i]])

训练过程中最有趣的部分是 get_gold_moves。 通过Goldbery 和 Nivre (2012),我们的语法解析器的性能可能会有所提升,他们曾指出我们错了很多年。

在词性标注文章中,我提醒大家,在训练期间,你要确保传递的是最后两个预测标记做为当前标记的特征,而不是最后两个黄金标记。测试期间只有预测标记,如果特征是基于训练过程中黄金序列的,训练环境就不会和测试环境保持一致,因此将会得到错误的权重。

在语法分析中我们面临的问题是不知道如何传递预测序列!通过采用黄金标准树结构,并发现可以转换为树的过渡序列,等等,使得训练得以工作,你获得返回的动作序列,保证执行运动,将得到黄金标准的依赖关系。

问题是,如果语法分析器处于任何没有沿着黄金标准序列的状态时,我们不知道如何教它做出的“正确”运动。一旦语法分析器发生了错误,我们不知道如何从实例中训练。

这是一个大问题,因为这意味着一旦语法分析器开始发生错误,它将停止在不属于训练数据的任何一种状态——导致出现更多的错误。

对于贪婪解析器而言,问题是具体的:一旦使用方向特性,有一种自然的方式做结构化预测。

像所有的最佳突破一样,一旦你理解了这些,解决方案似乎是显而易见的。我们要做的就是定义一个函数,此函数提问“有多少黄金标准依赖关系可以从这种状态恢复”。如果能定义这个函数,你可以依次进行每种运动,进而提问,“有多少黄金标准依赖关系可以从这种状态恢复?”。如果采用的操作可以让少一些的黄金标准依赖实现,那么它就是次优的。

这里需要领会很多东西。

因此我们有函数 Oracle(state):

Oracle(state) = | gold_arcs ∩ reachable_arcs(state) |

我们有一个操作集合,每种操作返回一种新状态。我们需要知道:

  shift_cost = Oracle(state) – Oracle(shift(state))
  right_cost = Oracle(state) – Oracle(right(state))
  left_cost = Oracle(state) – Oracle(left(state))

现在,至少一种操作返回0。Oracle(state)提问:“前进的最佳路径的成本是多少?”最佳路径的第一步是转移,向右,或者向左。

事实证明,我们可以得出 Oracle 简化了很多过渡系统。我们正在使用的过渡系统的衍生品 —— Arc Hybrid 是 Goldberg 和 Nivre (2013)提出的。

我们把oracle实现为一个返回0-成本的运动的方法,而不是实现一个功能的Oracle(state)。这可以防止我们做一堆昂贵的复制操作。希望代码中的推理不是太难以理解,如果感到困惑并希望刨根问底的花,你可以参考 Goldberg 和 Nivre 的论文。

def get_gold_moves(n0, n, stack, heads, gold):
  def deps_between(target, others, gold):
    for word in others:
      if gold[word] == target or gold[target] == word:
        return True
    return False

  valid = get_valid_moves(n0, n, len(stack))
  if not stack or (SHIFT in valid and gold[n0] == stack[-1]):
    return [SHIFT]
  if gold[stack[-1]] == n0:
    return [LEFT]
  costly = set([m for m in MOVES if m not in valid])
  # If the word behind s0 is its gold head, Left is incorrect
  if len(stack) >= 2 and gold[stack[-1]] == stack[-2]:
    costly.add(LEFT)
  # If there are any dependencies between n0 and the stack,
  # pushing n0 will lose them.
  if SHIFT not in costly and deps_between(n0, stack, gold):
    costly.add(SHIFT)
  # If there are any dependencies between s0 and the buffer, popping
  # s0 will lose them.
  if deps_between(stack[-1], range(n0+1, n-1), gold):
    costly.add(LEFT)
    costly.add(RIGHT)
  return [m for m in MOVES if m not in costly]

进行“动态 oracle”训练过程会产生很大的精度差异——通常为1-2%,和运行时的方式没有区别。旧的“静态oracle”贪婪训练过程已经完全过时;没有任何理由那样做了。
总结

我感觉,语言技术,特别是那些相关语法,特别神秘。我不能想象什么样的程序可以实现。

我认为对于人们来说,最好的解决方案可能相当复杂是很自然的。200,000 行的Java包感觉为宜。

但是,仅仅实现一个单一算法时,算法代码往往很短。当你只实现一种算法时,在写之前你确实知道要写什么,你不需要关注任何不必要的具有很大性能影响的抽象概念。
注释

[1] 我真的不确定如何计算Stanford解析器的代码行数。它的jar文件装载了200k大小内容,包括大量不同的模型。这并不重要,但在50k左右似乎是安全的。

[2]例如,如何解析“John's school of music calls”?你需要确认“John's school”短语和“John's school calls”、“John's school of music calls”有相同的结构。对可以放入短语的不同的“插槽”进行推理是我们推理句法分析的关键途径。你能想到每个短语为具有不同形状的连接器,你需要插入不同的插槽——每个短语也有一定数量不同形状的插槽。我们正试图弄清楚什么样的连接器在什么地方,因此可以搞清句子是如何连接在一起的。

[3]这里有使用了“深度学习”技术的 Stanford 解析器更新版本,此版本准确性更高。但是,最终模型的准确度仍排在最好的移进归约分析器后面。这是一篇伟大的文章,该想法在一个语法分析器上实现,这个语法分析器是不是最先进其实并不重要。

[4]一个细节:Stanford 依赖关系实际上是给定黄金标准短语结构树自动生成的。参考这里的Stanford依赖转换器页面:http://nlp.stanford.edu/software/stanford-dependencies.shtml。
无根据猜测

长期以来,增量语言处理算法是科学界的主要兴趣。如果你想要编写一个语法分析器来测试人类语句处理器如何工作的理论,那么,这个分析器需要建立部分解释器。这里有充分的证据,包括常识性反思,它设立我们不缓存的输入,说话者完成表达立即分析。

但与整齐的科学特征相比,当前算法胜出!尽我所能告诉大家,胜出的秘诀就是:

增量。早期的文字限制搜索。
    错误驱动。训练包含一个发生错误即更新的操作假设。

和人类语句处理的联系看起来诱人。我期待看到这些工程的突破是否带来一些心理语言学方面的进步。
参考书目

NLP 的文献几乎完全开放。所有相关论文都可以在这里找到:http://aclweb.org/anthology/。

我所描述的解析器是动态oracle arc-hybrid 系统的实现:

Goldberg, Yoav; Nivre, Joakim

Training Deterministic Parsers with Non-Deterministic Oracles

TACL 2013

然而,我编写了自己的特征。arc-hybrid 系统的最初描述在这里:

Kuhlmann, Marco; Gomez-Rodriguez, Carlos; Satta, Giorgio

Dynamic programming algorithms for transition-based dependency parsers

ACL 2011

这里最初描述了动态oracle训练方法:

A Dynamic Oracle for Arc-Eager Dependency Parsing

Goldberg, Yoav; Nivre, Joakim

COLING 2012

当Zhang 和 Clark 研究定向搜索时,这项工作依赖于以转换为基础的解析器在准确性上的重大突破。他们发表了很多论文,但首选的引用是:

Zhang, Yue; Clark, Steven

Syntactic Processing Using the Generalized Perceptron and Beam Search

Computational Linguistics 2011 (1)

另外一篇重要的文章是这个短篇的特征工程文章,这篇文章进一步提高了准确性:

Zhang, Yue; Nivre, Joakim

Transition-based Dependency Parsing with Rich Non-local Features

ACL 2011

作为定向解析器的学习框架,广义的感知器来自这篇文章

Collins, Michael

Discriminative Training Methods for Hidden Markov Models: Theory and Experiments with Perceptron Algorithms

EMNLP 2002

实验细节

文章开头的结果引用了华尔街日报语料库第22条。Stanford 解析器执行如下:

java -mx10000m -cp "$scriptdir/*:" edu.stanford.nlp.parser.lexparser.LexicalizedParser \
-outputFormat "penn" edu/stanford/nlp/models/lexparser/englishFactored.ser.gz $*

应用了一个小的后处理,撤销Stanford 解析器为数字添加的假设标记,使数字符合 PTB 标记:

"""Stanford parser retokenises numbers. Split them."""
import sys
import re

qp_re = re.compile('\xc2\xa0')
for line in sys.stdin:
  line = line.rstrip()
  if qp_re.search(line):
    line = line.replace('(CD', '(QP (CD', 1) + ')'
    line = line.replace('\xc2\xa0', ') (CD ')
  print line

由此产生的PTB格式的文件转换成使用 Stanford 转换器的依赖关系:

for f in $1/*.mrg; do
 echo $f
 grep -v CODE $f > "$f.2"
 out="$f.dep"
 java -mx800m -cp "$scriptdir/*:" edu.stanford.nlp.trees.EnglishGrammaticalStructure \
  -treeFile "$f.2" -basic -makeCopulaHead -conllx > $out
done

我不能轻易的读取了,但它应该只是使用相关文献的一般设置,将一个目录下的每个.mrg文件转换成一个CoNULL格式的 Stanford 基本依赖文件。

接着我从华尔街日报语料库第22条转换了黄金标准树进行评估。准确的分数是指所有未标记标识中未标记的附属分数(如弧头索引)

为了训练 parser.py,我将华尔街日报语料库 02-21 的黄金标准 PTB 树结构输出到同一个转换脚本中。

一言以蔽之,Stanford 模型和 parser.py 在同一组语句中进行训练,在我们知道答案的持有测试集上进行预测。准确性是指我们答对多少正确的语句首词。

在一个 2.4Ghz 的 Xeon 处理器上测试速度。我在服务器上进行了实验,为 Stanford 解析器提供了更多内存。parser.py 系统在我的MacBook Air上运行良好。在parser.py 的实验中,我使用了PyPy;与早期的基准相比,CPython大约快了一半。

parser.py 运行如此之快的一个原因是它进行未标记解析。根据以往的实验,经标记的解析器可能慢400倍,准确度提高大约1%。如果你能访问数据,使程序适应已标记的解析器对读者来说将是很好的锻炼机会。

RedShift 解析器的结果是从版本 b6b624c9900f3bf 取出的,运行如下:

./scripts/train.py -x zhang+stack -k 8 -p ~/data/stanford/train.conll ~/data/parsers/tmp
./scripts/parse.py ~/data/parsers/tmp ~/data/stanford/devi.txt /tmp/parse/
./scripts/evaluate.py /tmp/parse/parses ~/data/stanford/dev.conll
(0)

相关推荐

  • Python使用xlrd读取Excel格式文件的方法

    本文实例讲述了Python使用xlrd读取Excel格式文件的方法.分享给大家供大家参考.具体如下: 使用xlrd能够很方便的读取excel文件内容,而且这是个跨平台的库,能够在windows,linux/unix,等平台上面使用,代码如下: import xlrd fname = "sample.xls" bk = xlrd.open_workbook(fname) shxrange = range(bk.nsheets) try: sh = bk.sheet_by_name(&qu

  • 用Python进行一些简单的自然语言处理的教程

    本月的每月挑战会主题是NLP,我们会在本文帮你开启一种可能:使用pandas和python的自然语言工具包分析你Gmail邮箱中的内容. NLP-风格的项目充满无限可能: 情感分析是对诸如在线评论.社交媒体等情感内容的测度.举例来说,关于某个话题的tweets趋向于正面还是负面的意见?一个新闻网站涵盖的主题,是使用了更正面/负面的词语,还是经常与某些情绪相关的词语?这个"正面"的Yelp点评不是很讽刺么?(祝最后去的那位好运!) 分析语言在文学中的使用,进而衡量词汇或者写作风格随时间/

  • Python获取单个程序CPU使用情况趋势图

    本文定位:已将CPU历史数据存盘,等待可视化进行分析,可暂时没有思路. 前面一篇文章(http://www.jb51.net/article/61956.htm)提到过在linux下如何用python将top命令的结果进行存盘,本文是它的后续. python中我们可以用matplotlib很方便的将数据可视化,比如下面的代码: 复制代码 代码如下: import matplotlib.pyplot as plt list1 = [1,2,3] list2 = [4,5,9] plt.plot(l

  • 仅用500行Python代码实现一个英文解析器的教程

    语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理.自然语言引入了很多意外的歧义,以我们对世界的了解可以迅速地发现这些歧义.举一个我很喜欢的例子: 正确的解析是连接"with"和"pizza",而错误的解析将"with"和"eat"联系在了一起: 过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展.现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色. 文章剩下

  • 仅用50行Python代码实现一个简单的代理服务器

    之前遇到一个场景是这样的: 我在自己的电脑上需要用mongodb图形客户端,但是mongodb的服务器地址没有对外网开放,只能通过先登录主机A,然后再从A连接mongodb服务器B. 本来想通过ssh端口转发的,但是我没有从机器A连接ssh到B的权限.于是就自己用python写一个. 原理很简单. 1.开一个socket server监听连接请求 2.每接受一个客户端的连接请求,就往要转发的地址建一条连接请求.即client->proxy->forward.proxy既是socket服务端(监

  • 500行Python代码打造刷脸考勤系统

    需求分析 "员工刷脸考勤"系统,采用Python语言开发,可以通过摄像头添加员工面部信息,这里就涉及到两个具体的个问题,一个是应该以什么样的数据来标识每一个员工的面部信息,二是持久化地保存这些信息到数据库中去.更细地,还涉及表的设计;另一个基本要求是通过摄像头识别员工面部信息来完成考勤,这个问题基本可以通过遍历数据库里的员工面部数据与当前摄像头里的员工面部数据的比对来实现,但有一个问题就是假如摄像头里有多张人脸改怎么处理.扩展要求是导出每日的考勤表,可以拆分为两个部分,一个是存储考勤信

  • 仅用几行Python代码就能复制她的U盘文件?

    代码 # -*- coding:utf-8 -*- import os import time from datetime import datetime import shutil # U盘的盘符 usb_path = "E:/" # 要复制到的路径 save_path = "D:/haha" while (True):     if os.path.exists(usb_path):         shutil.copytree(usb_path, os.pa

  • 小 200 行 Python 代码制作一个换脸程序

    简介 在这篇文章中我将介绍如何写一个简短(200行)的 Python 脚本,来自动地将一幅图片的脸替换为另一幅图片的脸. 这个过程分四步: 检测脸部标记. 旋转.缩放.平移和第二张图片,以配合第一步. 调整第二张图片的色彩平衡,以适配第一张图片. 把第二张图像的特性混合在第一张图像中. 1.使用 dlib 提取面部标记 该脚本使用 dlib 的 Python 绑定来提取面部标记: Dlib 实现了 Vahid Kazemi 和 Josephine Sullivan 的<使用回归树一毫秒脸部对准>

  • 使用70行Python代码实现一个递归下降解析器的教程

     第一步:标记化 处理表达式的第一步就是将其转化为包含一个个独立符号的列表.这一步很简单,且不是本文的重点,因此在此处我省略了很多. 首先,我定义了一些标记(数字不在此中,它们是默认的标记)和一个标记类型: token_map = {'+':'ADD', '-':'ADD', '*':'MUL', '/':'MUL', '(':'LPAR', ')':'RPAR'} Token = namedtuple('Token', ['name', 'value']) 下面就是我用来标记 `expr` 表

  • 500行python代码实现飞机大战

    本文实例为大家分享了python代码实现飞机大战的具体代码,供大家参考,具体内容如下 import pygame import sys from pygame.sprite import Sprite from pygame.sprite import Group from time import sleep import pygame.font # 修改游戏的一些新设置 class Settings(): """存储<外星人入侵>的所有设置的类"&qu

  • 仅利用30行Python代码来展示X算法

    假如你对数独解法感兴趣,你可能听说过精确覆盖问题.给定全集 X 和 X 的子集的集合 Y ,存在一个 Y 的子集 Y*,使得 Y* 构成 X 的一种分割. 这儿有个Python写的例子. X = {1, 2, 3, 4, 5, 6, 7} Y = { 'A': [1, 4, 7], 'B': [1, 4], 'C': [4, 5, 7], 'D': [3, 5, 6], 'E': [2, 3, 6, 7], 'F': [2, 7]} 这个例子的唯一解是['B', 'D', 'F']. 精确覆盖问

  • 使用50行Python代码从零开始实现一个AI平衡小游戏

    集智导读: 本文会为大家展示机器学习专家 Mike Shi 如何用 50 行 Python 代码创建一个 AI,使用增强学习技术,玩耍一个保持杆子平衡的小游戏.所用环境为标准的 OpenAI Gym,只使用 Numpy 来创建 agent. 各位看官好,我(作者 Mike Shi--译者注)将在本文教大家如何用 50 行 Python 代码,教会 AI 玩一个简单的平衡游戏.我们会用到标准的 OpenAI Gym 作为测试环境,仅用 Numpy 创建我们的 AI,别的不用. 这个小游戏就是经典的

  • 5行Python代码实现图像分割的步骤详解

    众所周知图像是由若干有意义的像素组成的,图像分割作为计算机视觉的基础,对具有现有目标和较精确边界的图像进行分割,实现在图像像素级别上的分类任务. 图像分割可分为语义分割和实例分割两类,区别如下: 语义分割:将图像中每个像素赋予一个类别标签,用不同的颜色来表示: 实例分割:无需对每个像素进行标记,只需要找到感兴趣物体的边缘轮廓. 图像分割通常应用如下所示: 专业检测:应用于专业场景的图像分析,比如在卫星图像中识别建筑.道路.森林,或在医学图像中定位病灶.测量面积等: 智能交通:识别道路信息,包括车

随机推荐