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

Training IOB Chunker using nltk.tag.brill_trainer (Transformation-Based Learning)

我正在尝试使用 NLTK's brill module 来训练特定的词块划分器(为简单起见,假设是名词词块划分器)。我想使用三个功能,即。单词、POS 标签、IOB 标签。

我想将它们合并到 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)的完美捕捉。

nltk3 brill 训练器 api(我写的)确实处理用多维描述的标记序列的训练 功能,作为您的数据的示例。然而,实际限制可能很严格。多维学习中可能的模板数量 大幅增加,当前 nltk 实现的 brill tr​​ainer 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]