使用nltk.tag.brill_trainer(基于转换的学习)训练IOB Chunker



我正试图通过使用NLTK的brill模块来训练一个特定的分块器(为了简单起见,可以说是名词分块器)。我想使用三个功能,即单词,POS标签,IOB标签。

  • (Ramshaw和Marcus,1995:7)已经展示了100个模板,这些模板是由这三个特征的组合生成的,例如

    W0, P0, T0     # current word, pos tag, iob tag
    W-1, P0, T-1   # prev word, pos tag, prev iob tag
    ...
    

我想将它们合并到nltk.tbl.feature中,但只有两种类型的特征对象,即brill.Wordbrill.Pos。受设计限制,我只能像(word,POS)一样将word和POS功能放在一起,因此使用((word,POS),iob)作为训练的功能。例如,

from nltk.tbl import Template
from nltk.tag import brill, brill_trainer, untag
from nltk.corpus import treebank_chunk
from nltk.chunk.util import tree2conlltags, conlltags2tree
# Codes from (Perkins, 2013)
def train_brill_tagger(initial_tagger, train_sents, **kwargs):
templates = [
brill.Template(brill.Word([0])),
brill.Template(brill.Pos([-1])),
brill.Template(brill.Word([-1])),
brill.Template(brill.Word([0]),brill.Pos([-1])),]
trainer = brill_trainer.BrillTaggerTrainer(initial_tagger, templates, trace=3,)
return trainer.train(train_sents, **kwargs)
# generating ((word, pos),iob) pairs as feature.
def chunk_trees2train_chunks(chunk_sents):
tag_sents = [tree2conlltags(sent) for sent in chunk_sents]
return [[((w,t),c) for (w,t,c) in sent] for sent in tag_sents]
>>> from nltk.tag import DefaultTagger
>>> tagger = DefaultTagger('NN')
>>> train = treebank_chunk.chunked_sents()[:2]
>>> t = chunk_trees2train_chunks(train)
>>> bt = train_brill_tagger(tagger, t)
TBL train (fast) (seqs: 2; tokens: 31; tpls: 4; min score: 2; min acc: None)
Finding initial useful rules...
Found 79 useful rules.
B      |
S   F   r   O  |        Score = Fixed - Broken
c   i   o   t  |  R     Fixed = num tags changed incorrect -> correct
o   x   k   h  |  u     Broken = num tags changed correct -> incorrect
r   e   e   e  |  l     Other = num tags changed incorrect -> incorrect
e   d   n   r  |  e
------------------+-------------------------------------------------------
12  12   0  17  | NN->I-NP if Pos:NN@[-1]
3   3   0   0  | I-NP->O if Word:(',', ',')@[0]
2   2   0   0  | I-NP->B-NP if Word:('the', 'DT')@[0]
2   2   0   0  | I-NP->O if Word:('.', '.')@[0]

如上所示,(单词,pos)被视为一个整体特征。这并不是对三个功能(单词、pos标签、iob标签)的完美捕捉。

  • 是否有其他方法可以将word、pos、iob功能单独实现到nltk.tbl.feature
  • 如果这在NLTK中是不可能的,那么在python中还有其他实现吗?我只能在互联网上找到C++和Java的实现

nltk3 brill训练器api(我写的)确实处理用多维描述的令牌序列的训练功能,您的数据就是一个例子。然而,实际限制可能很严重。多维学习中可能的模板数量急剧增加,brill训练器的当前nltk实现会占用内存关于速度,类似于Ramshaw和Marcus 1994年的"探索变换规则序列的统计推导…"。内存消耗可能很大向系统提供比它可以处理。一个有用的策略是排名模板,根据它们生成良好规则的频率(请参见print_template_statistics())。通常,你可以放弃得分最低的分数(比如50-90%)表现几乎没有损失,训练时间大幅减少。

另一种或附加的可能性是使用nltkBrill的原始算法的实现,该算法具有非常不同的内存速度权衡;它不进行索引,因此将使用更少的内存。它使用了一些优化,实际上很快就找到了最好的规则,但在训练结束时,当有很多竞争对手、得分低的候选人时,通常速度非常慢。有时候你根本不需要这些。出于某种原因,这个实现似乎从新的nltks中被省略了,但这里是源代码(我刚刚测试过它)http://www.nltk.org/_modules/nltk/tag/brill_trainer_orig.html.

还有其他具有其他权衡的算法,以及特别是Florian和Ngai 2000的快速内存高效索引算法(http://www.aclweb.org/anthology/N/N01/N01-1006.pdf)以及Samuel 1998的概率规则抽样(https://www.aaai.org/Papers/FLAIRS/1998/FLAIRS98-045.pdf)将是一个有用的补充。此外,正如您所注意到的,文档并不完整,过于关注词性标记,也不清楚如何从中归纳。修复文档(也)在待办事项列表中。

然而,人们对nltk中的广义(非POS标记)tbl的兴趣相当有限(nltk2的完全不合适的api 10年来一直没有受到影响),所以不要屏住呼吸。如果你不耐烦了,你可能希望看看更专门的替代品,特别是mutbl和fntbl(在谷歌上搜索它们,我只知道有两个链接)。

无论如何,这里有一个nltk:的快速草图

首先,nltk中的一个硬编码约定是标记序列("标记"表示任何标签你想分配给你的数据,不一定是词性)作为成对的序列,[(token1,tag1),(token2,tag2),…]。标签是字符串;在里面许多基本应用程序,令牌也是如此。例如,标记可以是单词字符串的POS,如

[('And', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('completely', 'RB'), ('different', 'JJ')]

(顺便说一句,这种令牌-标签对序列约定在nltk和它的文档,但可以说它应该更好地表示为命名元组而不是配对,这样就不用说

[token for (token, _tag) in tagged_sequence]

你可以说例如

[x.token for x in tagged_sequence]

第一种情况在非对上失败,但第二种情况利用了duck类型,因此tagged_sequence可以是用户定义实例的任何序列,只要它们有一个属性"token"。)

现在,你很可能会对你的代币有更丰富的表达处置现有的标记器接口(nltk.tag.api.FeaturesetTaggerI)需要每个标记都是一个特征集,而不是一个字符串,这是一个映射的字典要素名称到序列中每个项目的要素值。

标记序列可能看起来像

[({'word': 'Pierre', 'tag': 'NNP', 'iob': 'B-NP'}, 'NNP'),
({'word': 'Vinken', 'tag': 'NNP', 'iob': 'I-NP'}, 'NNP'),
({'word': ',',      'tag': ',',   'iob': 'O'   }, ','),
...
]

还有其他的可能性(尽管nltk的其余部分支持较少)。例如,可以为每个令牌指定一个命名元组,或者用户定义类,允许您将任意数量的动态计算添加到属性访问(可能使用@property来提供一致的接口)。

brill tagger不需要知道您当前提供的视图在您的代币上。但是,它确实需要您提供一个初始标签它可以将表示中的令牌序列转换为标签。您不能直接使用nltk.tag.sequential中现有的标记符,因为他们期望[(word,tag),…]。但你仍然可以利用他们。下面的示例使用此策略(在MyInitialTagger中),并使用标记作为featureset字典视图。

from __future__ import division, print_function, unicode_literals
import sys
from nltk import tbl, untag
from nltk.tag.brill_trainer import BrillTaggerTrainer
# or: 
# from nltk.tag.brill_trainer_orig import BrillTaggerTrainer
# 100 templates and a tiny 500 sentences (11700 
# tokens) produce 420000 rules and uses a 
# whopping 1.3GB of memory on my system;
# brill_trainer_orig is much slower, but uses 0.43GB
from nltk.corpus import treebank_chunk
from nltk.chunk.util import tree2conlltags
from nltk.tag import DefaultTagger

def get_templates():
wds10 = [[Word([0])],
[Word([-1])],
[Word([1])],
[Word([-1]), Word([0])],
[Word([0]), Word([1])],
[Word([-1]), Word([1])],
[Word([-2]), Word([-1])],
[Word([1]), Word([2])],
[Word([-1,-2,-3])],
[Word([1,2,3])]]
pos10 = [[POS([0])],
[POS([-1])],
[POS([1])],
[POS([-1]), POS([0])],
[POS([0]), POS([1])],
[POS([-1]), POS([1])],
[POS([-2]), POS([-1])],
[POS([1]), POS([2])],
[POS([-1, -2, -3])],
[POS([1, 2, 3])]]
iobs5 = [[IOB([0])],
[IOB([-1]), IOB([0])],
[IOB([0]), IOB([1])],
[IOB([-2]), IOB([-1])],
[IOB([1]), IOB([2])]]

# the 5 * (10+10) = 100 3-feature templates 
# of Ramshaw and Marcus
templates = [tbl.Template(*wdspos+iob) 
for wdspos in wds10+pos10 for iob in iobs5]
# Footnote:
# any template-generating functions in new code 
# (as opposed to recreating templates from earlier
# experiments like Ramshaw and Marcus) might 
# also consider the mass generating Feature.expand()
# and Template.expand(). See the docs, or for 
# some examples the original pull request at
# https://github.com/nltk/nltk/pull/549 
# ("Feature- and Template-generating factory functions")
return templates
def build_multifeature_corpus():
# The true value of the target fields is unknown in testing, 
# and, of course, templates must not refer to it in training.
# But we may wish to keep it for reference (here, truepos).
def tuple2dict_featureset(sent, tagnames=("word", "truepos", "iob")):
return (dict(zip(tagnames, t)) for t in sent)
def tag_tokens(tokens):
return [(t, t["truepos"]) for t in tokens]
# connlltagged_sents :: [[(word,tag,iob)]]
connlltagged_sents = (tree2conlltags(sent) 
for sent in treebank_chunk.chunked_sents())
conlltagged_tokenses = (tuple2dict_featureset(sent) 
for sent in connlltagged_sents)
conlltagged_sequences = (tag_tokens(sent) 
for sent in conlltagged_tokenses)
return conlltagged_sequences
class Word(tbl.Feature):
@staticmethod
def extract_property(tokens, index):
return tokens[index][0]["word"]
class IOB(tbl.Feature):
@staticmethod
def extract_property(tokens, index):
return tokens[index][0]["iob"]
class POS(tbl.Feature):
@staticmethod
def extract_property(tokens, index):
return tokens[index][1]

class MyInitialTagger(DefaultTagger):
def choose_tag(self, tokens, index, history):
tokens_ = [t["word"] for t in tokens]
return super().choose_tag(tokens_, index, history)

def main(argv):
templates = get_templates()
trainon = 100
corpus = list(build_multifeature_corpus())
train, test = corpus[:trainon], corpus[trainon:]
print(train[0], "n")
initial_tagger = MyInitialTagger('NN')
print(initial_tagger.tag(untag(train[0])), "n")
trainer = BrillTaggerTrainer(initial_tagger, templates, trace=3)
tagger = trainer.train(train)
taggedtest = tagger.tag_sents([untag(t) for t in test])
print(test[0])
print(initial_tagger.tag(untag(test[0])))
print(taggedtest[0])
print()
tagger.print_template_statistics()
if __name__ == '__main__':
sys.exit(main(sys.argv))

上面的设置构建了一个POS标记器。如果你想针对另一个属性,比如说构建一个IOB标记器,你需要一些小的改变以便目标属性(您可以将其视为读写属性)从语料库中的"标记"位置访问[(token,tag),…]以及任何其他属性(可以认为是只读的)从"token"位置访问。例如:

1) 为IOB标记构建你的语料库[(token,tag),(token、tag),…]

def build_multifeature_corpus():
...
def tuple2dict_featureset(sent, tagnames=("word", "pos", "trueiob")):
return (dict(zip(tagnames, t)) for t in sent)
def tag_tokens(tokens):
return [(t, t["trueiob"]) for t in tokens]
...

2) 相应地更改初始标签机

...
initial_tagger = MyInitialTagger('O')
...

3) 修改特征提取类定义

class POS(tbl.Feature):
@staticmethod
def extract_property(tokens, index):
return tokens[index][0]["pos"]
class IOB(tbl.Feature):
@staticmethod
def extract_property(tokens, index):
return tokens[index][1]

最新更新