2016-06-22 5 views
2

Ich versuche, einen bestimmten Chunker (sagen wir ein Nomen chunker zur Einfachheit) mit NLTK's brill module zu trainieren. Ich würde gerne drei Funktionen verwenden, dh. Wort, POS-Tag, IOB-Tag.Training IOB Chunker mit nltk.tag.brill_trainer (Transformation-Based Learning)

  • (Ramshaw and Marcus, 1995:7) haben 100 Vorlagen gezeigt, die sich aus der Kombination dieser drei Merkmale erzeugt werden, beispielsweise

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

ich sie in nltk.tbl.feature integrieren möchten, aber es gibt nur zwei Arten von Feature-Objekten, dh. brill.Word und brill.Pos. Begrenzt durch das Design, konnte ich nur Wort- und POS-Merkmale wie (Wort, Pos) und somit (Wort, Pos), iob) als Merkmale für das Training zusammensetzen. Zum Beispiel

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:[email protected][-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] 

Wie oben gezeigt, werden (Wort, Pos) ein Merkmal als Ganzes behandelt. Dies ist keine perfekte Erfassung von drei Merkmalen (Wort, Pos-Tag, Iob-Tag).

  • Gibt es noch andere Möglichkeiten, Word-, Pos- und iob-Funktionen separat in nltk.tbl.feature zu implementieren?
  • Wenn es in NLTK unmöglich ist, gibt es andere Implementierungen von ihnen in Python? Ich konnte nur C++ und Java-Implementierungen im Internet finden.

Antwort

2

Der nltk3 Glattbutt Trainer api (ich es geschrieben habe) tut Training auf Sequenzen mit mehrdimensionalen beschriebenen Merkmale Token handhaben, wie Sie Ihre Daten ein Beispiel ist. Die praktischen Grenzen können jedoch schwerwiegend sein. Die Anzahl der möglichen Vorlagen beim multidimensionalen Lernen steigt drastisch an, und die aktuelle nltk-Implementierung des Brill-Trainers tauscht Speicher mit für Geschwindigkeit, ähnlich wie Ramshaw und Marcus 1994, "Erforschen der statistischen Ableitung von Transformationsregelsequenzen ...". Speicherverbrauch kann RIESIG sein und es ist sehr einfach, das System mehr Daten und/oder Vorlagen als kann es behandeln. Eine sinnvolle Strategie besteht darin, die Vorlagen nach der Häufigkeit zu ordnen, mit der sie gute Regeln erzeugen (siehe Beispiel print_template_statistics()). In der Regel können Sie die niedrigste Bewertung (z. B. 50-90%) mit wenig oder ohne Verlust der Leistung und einer erheblichen Verringerung der Trainingszeit verwerfen.

Eine andere oder zusätzliche Möglichkeit besteht darin, die Implementierung des ursprünglichen Algorithmus von Brill zu verwenden, der sehr unterschiedliche Speicher-Geschwindigkeits-Kompromisse hat; Es wird nicht indiziert und benötigt daher weniger Speicher. Es nutzt einige Optimierungen und ist eigentlich ziemlich schnell darin, die allerbesten Regeln zu finden, aber es ist im Allgemeinen sehr langsam gegen Ende des Trainings, wenn es viele konkurrierende Kandidaten mit niedriger Punktzahl gibt. Manchmal brauchst du diese sowieso nicht. Aus irgendeinem Grund scheint diese Implementierung in neueren nltks weggelassen worden zu sein, aber hier ist die Quelle (ich habe sie gerade getestet) .

Es gibt auch andere Algorithmen mit anderen Abwägungen und insbesondere die schnellen speichereffizienten Indizierung Algorithmen von Florian und Ngai 2000 (http://www.aclweb.org/anthology/N/N01/N01-1006.pdf) und probabilistischen Regel Probenahme von Samuel 1998 (https://www.aaai.org/Papers/FLAIRS/1998/FLAIRS98-045.pdf) wäre eine sinnvolle Ergänzung sein.Wie Sie auch bemerkt haben, ist die Dokumentation nicht vollständig und zu sehr auf die Wort-zu-Wort-Kennzeichnung ausgerichtet, und es ist nicht klar, wie man daraus generalisieren kann. Fixieren der Dokumente ist (auch) auf der Todo-Liste.

Allerdings war das Interesse für verallgemeinerte (nicht-POS-Tagging) Tbl in Nltk eher begrenzt (die völlig ungeeignete API von Nltk2 war seit 10 Jahren unberührt), also halten Sie nicht die Luft an. Wenn Sie ungeduldig werden, möchten Sie vielleicht mehr dedizierte Alternativen, insbesondere mutbl und fntbl (google ihnen, ich habe nur Reputation für zwei Links).

Wie auch immer, hier ist eine schnelle Skizze für nltk:

Zuerst wird eine hartkodierte Konvention in nltk ist, dass markierten Sequenzen (‚Tags‘ ein beliebiges Label bedeutet Sie Ihre Daten zuordnen möchten, nicht unbedingt kofinanzieren of-Speech) werden als Sequenzen von Paaren dargestellt, [(token1, tag1), (token2, tag2), ...]. Die Tags sind Strings; in viele grundlegende Anwendungen, so sind die Token. Zum Beispiel kann die Token Worte und die Saiten ihrer POS, wie in

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

(Als beiseite, diese Sequenz-of-Token-Tag-Paare Konvention ist allgegenwärtig in nltk und seine Dokumentation, aber es sollte wohl besser sein als benannte Tupel ausgedrückt eher als Paare, so dass anstelle von

[token for (token, _tag) in tagged_sequence] 

sagen Sie zum Beispiel sagen konnte

[x.token for x in tagged_sequence] 

Der erste Fall nicht auf nicht-Paare, aber die zweite Taten duck typing so die tagged_sequence jede Sequenz von benutzerdefinierten Instanzen sein könnte, solange sie haben ein Attribut „Token“.)

Nun Sie könnten eine reichere Darstellung dessen haben, was ein Token bei Ihrer Entsorgung ist. Eine vorhandene Tagger-Schnittstelle (nltk.tag.api.FeaturesetTaggerI) erwartet jedes Token als Feature-Set und nicht als String. Dies ist ein Wörterbuch, das Feature-Namen zu Feature-Werten für jedes Element in der Sequenz zuordnet.

Eine markierte Sequenz kann dann folgendermaßen aussehen

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

Es andere Möglichkeiten sind (wenn auch mit weniger Unterstützung in der übrigen nltk). Zum Beispiel könnten Sie ein benanntes Tupel für jedes Token oder eine benutzerdefinierte Klasse haben, mit der Sie einen beliebigen Betrag der dynamischen Berechnung zu Attributzugriff hinzufügen können (vielleicht mit @property, um eine konsistente Schnittstelle anzubieten).

Der Brill-Tagger muss nicht wissen, welche Ansicht Sie derzeit auf Ihren Tokens bereitstellen. Es ist jedoch erforderlich, dass Sie einen initialen Tagger bereitstellen, der Sequenzen von Tokens in Ihrer Repräsentation in Sequenzen von Tags aufnehmen kann. Sie können die vorhandenen Tagger in nltk.tag.sequential nicht direkt verwenden, da sie [(word, tag), ...] erwarten. Aber Sie können möglicherweise noch ausnutzen. Im folgenden Beispiel wird diese Strategie (in MyInitialTagger) und die Token-als-FeatureSet-Dictionary-Ansicht verwendet.

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)) 

Das obige Setup erstellt einen POS-Tagger.Wenn Sie stattdessen ein anderes Attribut anvisieren möchten, z. B. einen IOB-Tagger erstellen, benötigen Sie einige kleine Änderungen , damit auf das Zielattribut (das Sie sich als Lesen-Schreiben vorstellen können) von der 'Tag'-Position aus zugegriffen wird Ihr Korpus [(Token, Tag), ...] und alle anderen Attribute (die Sie sich als schreibgeschützt vorstellen können) werden von der Position 'token' aus aufgerufen. Zum Beispiel:

1) konstruiert Ihren Korpus [(Token-Tag), (Token-Tag), ...] für IOB Tagging

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) entsprechend den Anfang Tagger ändern

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

3) ändern sich die Merkmalsextraktionsklassendefinitionen

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]