Kneser-Ney smoothing を実装しようと調べていたところ、NLTKで実装されていたのでNLTKのngram言語モデルの使い方についてまとめます。
前処理にちょっと癖があるものの、エントロピーなど数値の算出が共通化されているのでモデルごとの違いを比較しやすい気がします。
詳細なコードはgistに記載しています。
nltk.lm
nltk.lm.models にLidstone, Laplaceといった基礎的なngram言語モデルが揃っていて、 自分が実装しようとしていた Kneser-Ney smoothingはnltk.lm.models.KneserNeyInterpolatedで実装されています。
NLTKはNLPの (機械学習でない) 実装はだいたい揃っているのですが、モジュールが多いため目的の関数を探すのが大変で、しかもドキュメントが整備されていたりされていなかったりするのでちょっととっつきにくい部分があります。
(実際自分は nltk.lm.smoothing.KneserNey がモデルだと勘違いしました。これはスムージングだけで、ngramモデルとしてはnltk.lm.KneserNeyInterpolated を使います。)
入力データの準備
NLTK :: Sample usage for lm を見ると lm.fit
で学習データを渡していますが、複数のモデルを試す場合、内部で毎回同じ計算をすることになるので少し工夫します。
>>> from nltk.lm import WittenBellInterpolated >>> lm = WittenBellInterpolated(ngram_order) >>> lm.fit(train_data, vocab_data)
nltk.lm.models
に実装されている言語モデルは nltk.lm.api.LanguageModel を継承しているため、こっちのドキュメントに従って入力データを準備していきます。
具体的には ngram_order
(orderと略されていることもある) と nltk.lm.vocabulary.Vocabulary と nltk.lm.counter.NgramCounter です。
Vocabularyは語彙辞書で、初期値に collections.Counter
が必要です。
NgramCounter は複数のngramに対応したカウント辞書なので、あらかじめngram_order を決めて、データのngramを求める必要があります。
複数のngramを求める nltk.util.everygrams があるので活用します。
ちなみに、NLTK :: Sample usage for lm で padded_everygram_pipeline を使っているので、このpadding に合わせてnltk.lm.preprocessing.pad_both_ends でpaddingしています。
from nltk.util import ngrams, everygrams from nltk.lm.vocabulary import Vocabulary from nltk.lm.counter import NgramCounter from nltk.lm.preprocessing import pad_both_ends ngram_order = 2 counts = Counter() ngram_text = [] train_iter = PennTreebank(split='train') for data in train_iter: words = list(pad_both_ends(tokenizer(data),n=ngram_order)) counts.update(words) ngrams = everygrams(words, max_len=ngram_order) ngram_text.append(ngrams) vocab = Vocabulary(counts=counts, unk_cutoff=1) counter = NgramCounter(ngram_text)
モデルを動かす
データの準備ができればあとはモデルを動かすだけです。学習の必要はないので。
ngramデータを渡せば entropyや perplexity も計算できます。
nltk.utilにngramやbigramデータを求める関数があるのでngramデータも簡単に作成できます。
from nltk.lm import WittenBellInterpolated from nltk.util import bigrams # ngram_order = 2 lm = WittenBellInterpolated(ngram_order, vocabulary=vocab, counter=counter) sent = "this is a sentence" sent_pad = list(bigrams(pad_both_ends(tokenizer(sent), n=ngram_order))) print(sent_pad) lm.entropy(sent_pad) # 7.662118839369174
Gist
Gistでは実装されている言語モデルについてPentreeBank データでエントロピーを比較しました。
KneserNeyInterpolated だけ異常に重かったです。 Kneser-Ney smoothingの仕組みと実装コードをみればそりゃそうだろなって気持ちなのですが、別のブログ記事としてまとめようと思います。