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

Python 使用nltk.tag.brill_trainer培训IOB Chunker(基于转换的学习),python,nltk,pos-tagger,text-chunking,Python,Nltk,Pos Tagger,Text Chunking,我试图通过使用来训练一个特定的chunker(为了简单起见,让我们说一个名词chunker)。我想使用三种功能,即word、POS标签、IOB标签 显示了100个模板,这些模板是由这三个功能的组合生成的,例如 W0, P0, T0 # current word, pos tag, iob tag W-1, P0, T-1 # prev word, pos tag, prev iob tag ... from nltk.tbl import Template from nltk.t

我试图通过使用来训练一个特定的chunker(为了简单起见,让我们说一个名词chunker)。我想使用三种功能,即word、POS标签、IOB标签

  • 显示了100个模板,这些模板是由这三个功能的组合生成的,例如

    W0, P0, T0     # current word, pos tag, iob tag
    W-1, P0, T-1   # prev word, pos tag, prev iob tag
    ...
    
    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),然后使用((word,POS),iob)作为培训功能。比如说,

W0, P0, T0     # current word, pos tag, iob tag
W-1, P0, T-1   # prev word, pos tag, prev iob tag
...
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标记、iob标记)的完美捕获

  • 将word、pos、iob功能分别实现为
    nltk.tbl.feature
    的任何其他方法
  • 如果这在NLTK中是不可能的,那么在python中还有其他实现吗?我只能在互联网上找到C++和java实现。
nltk3 brill trainer api(我编写的)确实可以处理使用多维标记描述的令牌序列的培训 功能,因为您的数据就是一个示例。然而,实际限制可能很严格。多维学习中可能的模板数量 大幅增加,且brill trainer当前的nltk实现可交换内存 对于速度,类似于Ramshaw和Marcus 1994,“探索转换规则序列的统计推导…”。 内存消耗可能是巨大的,而且 给系统提供比以前更多的数据和/或模板非常容易 它可以处理。一个有用的策略是排名 根据生成良好规则的频率设置模板(请参见 打印模板(下例中的统计信息()。 通常,你可以放弃最低分数(比如50-90%) 在性能上几乎没有损失,训练时间大大缩短

另一种或额外的可能性是使用nltk Brill原始算法的实现,该算法具有非常不同的内存速度权衡;它不编制索引,因此将使用更少的内存。它使用了一些优化,实际上很快就能找到最好的规则,但在训练结束时,当有许多竞争对手、得分较低的候选人时,通常速度非常慢。不管怎样,有时候你并不需要这些。出于某种原因,新的NLTK中似乎省略了这个实现,但这里是源代码(我刚刚测试了它)

还有其他算法与其他折衷,以及 特别是Florian和Ngai 2000的快速内存高效索引算法 ()及 Samuel 1998的概率规则抽样 ()将是一个有用的补充。另外,正如您所注意到的,文档并不完整,而且过于关注词性标记,并且不清楚如何从中概括。修复文档也在待办事项列表中

然而,人们对nltk中的广义(非词性标记)tbl的兴趣相当有限(nltk2的完全不合适的api已经10年没有被触及),所以不要屏息以待。如果您不耐烦,您可能希望查看更多专用的替代方案, 特别是mutbl和fntbl(谷歌他们,我只知道两个链接)

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

首先,nltk中的硬编码约定是标记序列(“标记”表示任何标签) 您希望分配给您的数据(不一定是词性)表示 作为成对序列,[(标记1,标记1),(标记2,标记2),…]。标签是字符串;在里面 许多基本应用程序,令牌也是如此。例如,标记可以是单词 以及字符串的位置,如

[('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类型 标记的_序列可以是用户定义实例的任何序列,只要 它们有一个属性“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标记器不需要知道您当前提供的视图 在你的代币上。但是,它确实需要您提供初始标记器 它可以将表示中的令牌序列转换为 标签。不能直接使用nltk.tag.sequential中的现有标记器, 因为他们期望[(单词,标签),…]。但你还是可以 利用他们。下面的示例使用此策略(在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标记器,则需要进行一些小的更改 因此,目标属性(您可以将其视为读写) 从语料库中的“标记”位置访问[(标记,标记),…] 以及任何其他属性(您可以将其视为只读) 从“令牌”位置访问。例如:

1) 为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 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]