使用 nltk.tag.brill_trainer 训练 IOB Chunker(基于转换的学习)
Training IOB Chunker using nltk.tag.brill_trainer (Transformation-Based Learning)
我正在尝试使用 NLTK's brill module 来训练特定的词块划分器(为简单起见,假设是名词词块划分器)。我想使用三个功能,即。单词、POS 标签、IOB 标签。
(Ramshaw and 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
, but there are only two kinds of feature objects, ie. brill.Word
and brill.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]
如上所示,(word, pos)是作为一个整体来对待一个特征的。这不是三个特征(word、pos-tag、iob-tag)的完美捕捉。
- 还有其他方法可以将 word、pos、iob 功能分别实现到
nltk.tbl.feature
中吗?
- 如果在 NLTK 中是不可能的,那么在 python 中是否有它们的其他实现?我只能在互联网上找到 C++ 和 Java 实现。
nltk3 brill 训练器 api(我写的)确实处理用多维描述的标记序列的训练
功能,作为您的数据的示例。然而,实际限制可能很严格。多维学习中可能的模板数量
大幅增加,当前 nltk 实现的 brill trainer trades memory
对于速度,类似于 Ramshaw 和 Marcus 1994,"Exploring the statistical derivation of transformation-rule sequences..."。
内存消耗可能很大
给系统更多的数据 and/or 模板很容易
它可以处理。一个有用的策略是排名
模板根据它们产生良好规则的频率(见
print_template_statistics() 在下例中)。
通常,您可以丢弃得分最低的分数(比如 50-90%)
性能损失很小或没有损失,训练时间大大减少。
另一种或额外的可能性是使用 nltk
Brill 原始算法的实现,它具有非常不同的内存速度权衡;它没有索引,因此会使用更少的内存。它使用了一些优化,实际上在找到最佳规则方面相当快,但是当有许多竞争性的、低分的候选人时,它通常在训练结束时非常慢。无论如何,有时候你不需要那些。出于某种原因,这个实现似乎已从较新的 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 中广义(非词性标注)tbl 的兴趣一直相当有限(nltk2 的完全不适合 api 10 年未触及),所以不要屏住呼吸.如果你不耐烦,你可能希望查看更多专用的替代品,
特别是 mutbl 和 fntbl(google 它们,我只有两个链接的声誉)。
无论如何,这是 nltk 的速写:
首先,nltk 中的硬编码约定是标记序列('tags' 表示任何标签
你想分配给你的数据,不一定是词性)代表
作为成对序列,[(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]
第一种情况在非对上失败,但第二种情况利用鸭子打字所以
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 的其余部分支持较少)。
例如,您可以为每个标记都有一个命名的元组,或者一个用户定义的
class 允许您添加任意数量的动态计算
属性访问(可能使用@属性 来提供一致的接口)。
brill 标注器不需要知道您当前提供的视图
在你的代币上。但是,它确实需要您提供初始标记器
它可以将表示中的标记序列转换为
标签。您不能直接使用 nltk.tag.sequential 中现有的标注器,
因为他们期望 [(word, tag), ...]。但你仍然可以
利用他们。下面的例子使用了这个策略(在 MyInitialTagger 中),以及 token-as-featureset-dictionary 视图。
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))
上面的设置构建了一个词性标注器。如果您希望以另一个属性为目标,比如构建一个 IOB 标记器,则需要进行一些小的更改
这样目标属性(你可以认为是读写的)
从语料库中的 'tag' 位置访问 [(token, tag), ...]
和任何其他属性(您可以将其视为只读)
从 'token' 位置访问。例如:
1) 构建语料库 [(token,tag), (token,tag), ...] 用于 IOB 标记
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定义
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]
我正在尝试使用 NLTK's brill module 来训练特定的词块划分器(为简单起见,假设是名词词块划分器)。我想使用三个功能,即。单词、POS 标签、IOB 标签。
(Ramshaw and 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
, but there are only two kinds of feature objects, ie. brill.Word
and brill.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]
如上所示,(word, pos)是作为一个整体来对待一个特征的。这不是三个特征(word、pos-tag、iob-tag)的完美捕捉。
- 还有其他方法可以将 word、pos、iob 功能分别实现到
nltk.tbl.feature
中吗? - 如果在 NLTK 中是不可能的,那么在 python 中是否有它们的其他实现?我只能在互联网上找到 C++ 和 Java 实现。
nltk3 brill 训练器 api(我写的)确实处理用多维描述的标记序列的训练 功能,作为您的数据的示例。然而,实际限制可能很严格。多维学习中可能的模板数量 大幅增加,当前 nltk 实现的 brill trainer trades memory 对于速度,类似于 Ramshaw 和 Marcus 1994,"Exploring the statistical derivation of transformation-rule sequences..."。 内存消耗可能很大 给系统更多的数据 and/or 模板很容易 它可以处理。一个有用的策略是排名 模板根据它们产生良好规则的频率(见 print_template_statistics() 在下例中)。 通常,您可以丢弃得分最低的分数(比如 50-90%) 性能损失很小或没有损失,训练时间大大减少。
另一种或额外的可能性是使用 nltk Brill 原始算法的实现,它具有非常不同的内存速度权衡;它没有索引,因此会使用更少的内存。它使用了一些优化,实际上在找到最佳规则方面相当快,但是当有许多竞争性的、低分的候选人时,它通常在训练结束时非常慢。无论如何,有时候你不需要那些。出于某种原因,这个实现似乎已从较新的 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 中广义(非词性标注)tbl 的兴趣一直相当有限(nltk2 的完全不适合 api 10 年未触及),所以不要屏住呼吸.如果你不耐烦,你可能希望查看更多专用的替代品, 特别是 mutbl 和 fntbl(google 它们,我只有两个链接的声誉)。
无论如何,这是 nltk 的速写:
首先,nltk 中的硬编码约定是标记序列('tags' 表示任何标签 你想分配给你的数据,不一定是词性)代表 作为成对序列,[(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]
第一种情况在非对上失败,但第二种情况利用鸭子打字所以 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 的其余部分支持较少)。 例如,您可以为每个标记都有一个命名的元组,或者一个用户定义的 class 允许您添加任意数量的动态计算 属性访问(可能使用@属性 来提供一致的接口)。
brill 标注器不需要知道您当前提供的视图 在你的代币上。但是,它确实需要您提供初始标记器 它可以将表示中的标记序列转换为 标签。您不能直接使用 nltk.tag.sequential 中现有的标注器, 因为他们期望 [(word, tag), ...]。但你仍然可以 利用他们。下面的例子使用了这个策略(在 MyInitialTagger 中),以及 token-as-featureset-dictionary 视图。
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))
上面的设置构建了一个词性标注器。如果您希望以另一个属性为目标,比如构建一个 IOB 标记器,则需要进行一些小的更改 这样目标属性(你可以认为是读写的) 从语料库中的 'tag' 位置访问 [(token, tag), ...] 和任何其他属性(您可以将其视为只读) 从 'token' 位置访问。例如:
1) 构建语料库 [(token,tag), (token,tag), ...] 用于 IOB 标记
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定义
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]