特征集

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

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

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

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

  1. def extract_features(words, tags, n0, n, stack, parse):  
  2.     def get_stack_context(depth, stack, data):  
  3.         if depth >;= 3:  
  4.             return data[stack[-1]], data[stack[-2]], data[stack[-3]]  
  5.         elif depth >= 2:  
  6.             return data[stack[-1]], data[stack[-2]], '' 
  7.         elif depth == 1:  
  8.             return data[stack[-1]], '''' 
  9.         else:  
  10.             return '''''' 
  11.    
  12.     def get_buffer_context(i, n, data):  
  13.         if i + 1 >= n:  
  14.             return data[i], '''' 
  15.         elif i + 2 >= n:  
  16.             return data[i], data[i + 1], '' 
  17.         else:  
  18.             return data[i], data[i + 1], data[i + 2]  
  19.    
  20.     def get_parse_context(word, deps, data):  
  21.         if word == -1:  
  22.             return 0'''' 
  23.         deps = deps[word]  
  24.         valency = len(deps)  
  25.         if not valency:  
  26.             return 0'''' 
  27.         elif valency == 1:  
  28.             return 1, data[deps[-1]], '' 
  29.         else:  
  30.             return valency, data[deps[-1]], data[deps[-2]]  
  31.    
  32.     features = {}  
  33.     # Set up the context pieces --- the word, W, and tag, T, of:  
  34.     # S0-2: Top three words on the stack  
  35.     # N0-2: First three words of the buffer  
  36.     # n0b1, n0b2: Two leftmost children of the first word of the buffer  
  37.     # s0b1, s0b2: Two leftmost children of the top word of the stack  
  38.     # s0f1, s0f2: Two rightmost children of the top word of the stack  
  39.    
  40.     depth = len(stack)  
  41.     s0 = stack[-1if depth else -1 
  42.    
  43.     Ws0, Ws1, Ws2 = get_stack_context(depth, stack, words)  
  44.     Ts0, Ts1, Ts2 = get_stack_context(depth, stack, tags)  
  45.    
  46.     Wn0, Wn1, Wn2 = get_buffer_context(n0, n, words)  
  47.     Tn0, Tn1, Tn2 = get_buffer_context(n0, n, tags)  
  48.    
  49.     Vn0b, Wn0b1, Wn0b2 = get_parse_context(n0, parse.lefts, words)  
  50.     Vn0b, Tn0b1, Tn0b2 = get_parse_context(n0, parse.lefts, tags)  
  51.    
  52.     Vn0f, Wn0f1, Wn0f2 = get_parse_context(n0, parse.rights, words)  
  53.     _, Tn0f1, Tn0f2 = get_parse_context(n0, parse.rights, tags)  
  54.    
  55.     Vs0b, Ws0b1, Ws0b2 = get_parse_context(s0, parse.lefts, words)  
  56.     _, Ts0b1, Ts0b2 = get_parse_context(s0, parse.lefts, tags)  
  57.    
  58.     Vs0f, Ws0f1, Ws0f2 = get_parse_context(s0, parse.rights, words)  
  59.     _, Ts0f1, Ts0f2 = get_parse_context(s0, parse.rights, tags)  
  60.    
  61.     # Cap numeric features at 5?   
  62.     # String-distance  
  63.     Ds0n0 = min((n0 - s0, 5)) if s0 != 0 else 0 
  64.    
  65.     features['bias'] = 1 
  66.     # Add word and tag unigrams  
  67.     for w in (Wn0, Wn1, Wn2, Ws0, Ws1, Ws2, Wn0b1, Wn0b2, Ws0b1, Ws0b2, Ws0f1, Ws0f2):  
  68.         if w:  
  69.             features['w=%s' % w] = 1 
  70.     for t in (Tn0, Tn1, Tn2, Ts0, Ts1, Ts2, Tn0b1, Tn0b2, Ts0b1, Ts0b2, Ts0f1, Ts0f2):  
  71.         if t:  
  72.             features['t=%s' % t] = 1 
  73.    
  74.     # Add word/tag pairs  
  75.     for i, (w, t) in enumerate(((Wn0, Tn0), (Wn1, Tn1), (Wn2, Tn2), (Ws0, Ts0))):  
  76.         if w or t:  
  77.             features['%d w=%s, t=%s' % (i, w, t)] = 1 
  78.    
  79.     # Add some bigrams  
  80.     features['s0w=%s,  n0w=%s' % (Ws0, Wn0)] = 1 
  81.     features['wn0tn0-ws0 %s/%s %s' % (Wn0, Tn0, Ws0)] = 1 
  82.     features['wn0tn0-ts0 %s/%s %s' % (Wn0, Tn0, Ts0)] = 1 
  83.     features['ws0ts0-wn0 %s/%s %s' % (Ws0, Ts0, Wn0)] = 1 
  84.     features['ws0-ts0 tn0 %s/%s %s' % (Ws0, Ts0, Tn0)] = 1 
  85.     features['wt-wt %s/%s %s/%s' % (Ws0, Ts0, Wn0, Tn0)] = 1 
  86.     features['tt s0=%s n0=%s' % (Ts0, Tn0)] = 1 
  87.     features['tt n0=%s n1=%s' % (Tn0, Tn1)] = 1 
  88.    
  89.     # Add some tag trigrams  
  90.     trigrams = ((Tn0, Tn1, Tn2), (Ts0, Tn0, Tn1), (Ts0, Ts1, Tn0),   
  91.                 (Ts0, Ts0f1, Tn0), (Ts0, Ts0f1, Tn0), (Ts0, Tn0, Tn0b1),  
  92.                 (Ts0, Ts0b1, Ts0b2), (Ts0, Ts0f1, Ts0f2), (Tn0, Tn0b1, Tn0b2),  
  93.                 (Ts0, Ts1, Ts1))  
  94.     for i, (t1, t2, t3) in enumerate(trigrams):  
  95.         if t1 or t2 or t3:  
  96.             features['ttt-%d %s %s %s' % (i, t1, t2, t3)] = 1 
  97.    
  98.     # Add some valency and distance features  
  99.     vw = ((Ws0, Vs0f), (Ws0, Vs0b), (Wn0, Vn0b))  
  100.     vt = ((Ts0, Vs0f), (Ts0, Vs0b), (Tn0, Vn0b))  
  101.     d = ((Ws0, Ds0n0), (Wn0, Ds0n0), (Ts0, Ds0n0), (Tn0, Ds0n0),  
  102.         ('t' + Tn0+Ts0, Ds0n0), ('w' + Wn0+Ws0, Ds0n0))  
  103.     for i, (w_t, v_d) in enumerate(vw + vt + d):  
  104.         if w_t or v_d:  
  105.             features['val/d-%d %s %d' % (i, w_t, v_d)] = 1 
  106.     return features  

训练

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

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

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

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

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

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

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

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

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

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

这里需要领会很多东西。

因此我们有函数 Oraclestate):

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 的论文。

  1. def get_gold_moves(n0, n, stack, heads, gold):  
  2.     def deps_between(target, others, gold):  
  3.         for word in others:  
  4.             if gold[word] == target or gold[target] == word:  
  5.                 return True 
  6.         return False 
  7.    
  8.     valid = get_valid_moves(n0, n, len(stack))  
  9.     if not stack or (SHIFT in valid and gold[n0] == stack[-1]):  
  10.         return [SHIFT]  
  11.     if gold[stack[-1]] == n0:  
  12.         return [LEFT]  
  13.     costly = set([m for m in MOVES if m not in valid])  
  14.     # If the word behind s0 is its gold head, Left is incorrect  
  15.     if len(stack) >= 2 and gold[stack[-1]] == stack[-2]:  
  16.         costly.add(LEFT)  
  17.     # If there are any dependencies between n0 and the stack,  
  18.     # pushing n0 will lose them.  
  19.     if SHIFT not in costly and deps_between(n0, stack, gold):  
  20.         costly.add(SHIFT)  
  21.     # If there are any dependencies between s0 and the buffer, popping  
  22.     # s0 will lose them.  
  23.     if deps_between(stack[-1], range(n0+1, n-1), gold):  
  24.         costly.add(LEFT)  
  25.         costly.add(RIGHT)  
  26.     return [m for m in MOVES if m not in costly]  

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


评论关闭