サイコロ本 (統計的自然言語処理の基礎) で確率的言語モデル(ngram言語モデル) のバリエーションについて少し勉強したので、実装して比較してみます。
ngram言語モデル
長さnの単語列 において以下のように定義される確率 を推定するモデルのこと。 以下のように定義される。
ここで、 は条件付き確率。 1単語目から i-1 単語目までが である事象において、i単語目の単語が である確率を指す。
実際はマルコフ性を考慮し、1単語目から i-1 単語目ではなく、後ろn-1単語だけで推定することが多い。
このnがngramモデルのnにあたる。 n=2 なら bigram model, n=3 なら trigram modelになる。(n=1のときは語順を考慮しない)
数式右辺の は単語もしくは単語列の出現確率のため、頻度 に基づいて算出できる。
n=2のときは、
(Nは学習データの単語数)なので、条件付き確率は以下になる。
これがngram言語モデルのベースとなる最尤推定 (Maximam likelihood)モデル。
スムージング
単純な最尤推定だとデータスパースネスを考慮できない。頻度が0 (未知語) の場合、も0になってしまう。
考慮する手法が ディスカウント(discounting) , スムージング(smoothing, 平滑化) 。
ディスカウントは低頻度に対して確率を分けるため、それ以外の確率を割引くところから名付けられたらしい。 個人的にはスムージングの方が一般的だと思う。
さまざま提案されているので今回は簡単な手法を試してみる。
1-加算 (add-one)
について、分母と分子それぞれに1足す方法。
Lidstoneの法則
1だと大きすぎるのでいい感じの値 を設定する方法。
特にうまくいく の場合を 期待尤度推定 (expected likehood estimation: ELE) と呼ぶ。 (らしいが、古い手法だからか調べてもあまり参考文献が出てこない。)
実装
google colab 上で動かしてみる。
まとめたnotebookは URL にアップロードしました。
torchtext_ngram.ipynb · GitHub
前処理
データ
のちのちpytorchのモデルと比較したいので、torchtext のデータセットを活用する。
今回は torchtext.datasets.WikiText2 を利用。
元データは The WikiText Long Term Dependency Language Modeling Dataset。
The WikiText language modeling dataset is a collection of over 100 million tokens extracted from the set of verified Good and Featured articles on Wikipedia.
とある通り、Wikipediaデータ。
論文 ([1609.07843] Pointer Sentinel Mixture Models) を読む限り、前処理で出現回数が3回未満のvocabは <unk>
に置き換えているようです。
tokenizer
pytorchのtutorial Text classification with the torchtext library — PyTorch Tutorials 1.11.0+cu102 documentation に従って、tokenizerとvocabを設定する。
tokenizer は torchtext のget_tokenizer を使う。spacyを通すこともできるが、シンプルな normalize だけ行う "basic_english"
を指定。
tokenizer = get_tokenizer('basic_english')
vocab
build-vocab-from-iterator で語彙設定もしてしまう。
tokenizerで単語ごと分割したデータを渡すことで vocab を設定する。
vocab.set_default_index(vocab['<unk>'])
とすれば、 <unk>
以外の未知語も <unk>
扱いにできる。
train_iter = torchtext.datasets.WikiText2(split='train') vocab = build_vocab_from_iterator(map(tokenizer, train_iter), specials=['<unk>']) vocab.set_default_index(vocab['<unk>'])
count ngram
前処理が終わったところで、ngramデータを構築する。
簡単な頻度データしか使わないのでpure pythonで実装。unigram と bigramをカウントしていく。
文頭と文末に <bos>
と <eos>
を設定しカウントする場合もあるが、テキストをそのままカウントする。
train_iter = torchtext.datasets.WikiText2(split='train') # unigram と bigram を取得する unigram_counts = defaultdict(int) bigram_counts = defaultdict(int) for t in train_iter: tokens = text2index(t) for t in iter_ngram(tokens, 1): unigram_counts[t[0]] +=1 for t in iter_ngram(tokens, 2): bigram_counts[tuple(t)] +=1
エントロピーを算出
エントロピーを計算。対数尤度を単語数 (N_T) で割った値。
ただし
- テストデータ
- はデータ に対する model の出力
- テストデータの合計単語数
ngram言語モデルは
なので、
と変形でき、総和の総和でエントロピーを計算できる。
def get_probability(w1, w2, param=0): """ P(w_2 | w_1) を求める C(w1, w2) + param / C(w1) + param """ return float(bigram_counts.get((w1,w2), 0) + param) / (unigram_counts.get(w1,0) + param) def get_entropy(lambda_param=0): # 単語数 N_T word_sum = 0 # 対数尤度 h = 0 test_iter = torchtext.datasets.WikiText2(split='test') for t in test_iter: tokens = text2index(t) word_sum += len(tokens) for i in range(1, len(tokens)-1): p = get_probability( tokens[i-1], tokens[i], param=lambda_param ) h += -math.log2(p) print(f"word_sum: {word_sum}\nentropy: {h/word_sum}")
add-one(Laplace) と ELE(Lidstone) で比較してみる。
add-one
>> get_entropy(1) word_sum: 241859 entropy: 6.399829746902186
ELE
>> get_entropy(1/2) word_sum: 241859 entropy: 6.646151077249645
微妙な差とはいえ、add-oneよりいい結果になるはずのELEのほうがエントロピーが高くなってしまった。
なぜだろう……。どこかミスしているかもしれない。
カバレッジも計算しようかと思ったが、train データに <unk>
があるので厳密には算出できない気がする。
おわりに
今回は簡単なngram言語モデルを実装してみた。
ちなみに深層学習モデルの流行で、言語モデルといえばNLPのイメージがすっかり定着しているが、音声認識 (speech recognition) でも使われていることを久々に思い出した。
他のスムージング手法についても実装してみたいと思います。