【问题标题】:Training IOB Chunker using nltk.tag.brill_trainer (Transformation-Based Learning)使用 nltk.tag.brill_trainer 训练 IOB Chunker(基于转换的学习)
【发布时间】:2023-04-06 12:24:01
【问题描述】:

我正在尝试通过使用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中,但是只有两种特征对象,即。 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]

如上所示,(word, pos) 将一个特征视为一个整体。这不是对三个特征(word、pos-tag、iob-tag)的完美捕捉。

  • 还有其他方法可以将 word、pos、iob 功能分别实现到nltk.tbl.feature@中吗?
  • 如果在 NLTK 中不可能,那么在 python 中是否还有其他实现?我只能在 Internet 上找到 C++ 和 Java 实现。

【问题讨论】:

    标签: python nltk pos-tagger text-chunking


    【解决方案1】:

    nltk3 brill tr​​ainer api(我写的)确实处理用多维描述的标记序列的训练 功能,因为您的数据就是一个例子。然而,实际限制可能很严重。多维学习中可能的模板数量 急剧增加,并且 brill tr​​ainer 的当前 nltk 实现会交换内存 对于速度,类似于 Ramshaw 和 Marcus 1994,“探索转换规则序列的统计推导......”。 内存消耗可能是巨大的 为系统提供更多的数据和/或模板非常容易 它可以处理。一个有用的策略是排名 模板根据它们产生良好规则的频率(参见 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 中的广义(非 POS 标记)tbl 的兴趣相当有限(nltk2 完全不适合的 api 10 年没有被触及),所以不要屏住呼吸。如果您不耐烦,不妨查看更多专用的替代方案, 特别是 mutbl 和 fntbl (谷歌他们,我只有两个链接的声誉)。

    不管怎样,这里是 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 可以是任何用户定义的实例序列,只要 他们有一个属性“令牌”。)

    现在,您可以更丰富地表示您的令牌是什么 处理。现有的标注器接口 (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 标记器不需要知道您当前提供的视图 在你的代币上。但是,它确实需要您提供初始标记器 它可以将代表中的标记序列转换为 标签。您不能直接使用 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 标记器,则需要进行一些小的更改 以便目标属性(您可以将其视为读写) 从您的语料库中的“标签”位置访问 [(token, tag), ...] 和任何其他属性(您可以将其视为只读) 从“令牌”位置访问。例如:

    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]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-12-30
      • 2012-08-20
      • 1970-01-01
      • 2022-09-27
      • 1970-01-01
      • 2017-07-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多