gensimのTopicモデルでPerplexityを計算する

前回、scikit-learnのトピックモデル(LDA)における評価指標として、Perplexityを算出する方法を紹介しました。
参考: トピックモデルの評価指標Perplexityの実験

今回はgensim版です。
gensimのLDAモデルには log_perplexity と言うメソッドがあるので、それを使うだけです、って話であれば前の記事とまとめてしまってよかったのですが、
話はそう単純では無いので記事を分けました。

さて、 log_perplexity ってメソッドですが、いかにも perplexity の自然対数を返してくれそうなメソッドです。
perplexity が欲しかったら、 $\exp(log\_perplexity)$ を計算すれば良さそうに見えます。
しかし、 log_perplexity は perplexity の自然対数では無いと言う事実を確認できるのが次の実験です。

前回の記事と同じく、4つのトピックにそれぞれ5単語を含む架空の言語で実験します。


import numpy as np
from gensim.corpora.dictionary import Dictionary
from gensim.models import LdaModel


word_list = [
    ["white", "black", "red", "green", "blue"],
    ["dog", "cat", "fish", "bird", "rabbit"],
    ["apple", "banana", "lemon", "orange", "melon"],
    ["Japan", "America", "China", "England", "France"],
]
corpus_list = [
    np.random.choice(word_list[topic], 100)
    for topic in range(len(word_list)) for i in range(100)
]

# 単語と単語IDを対応させる辞書の作成
dictionary = Dictionary(corpus_list)
# LdaModelが読み込めるBoW形式に変換
corpus = [dictionary.doc2bow(text) for text in corpus_list]

# トピック数4を指定して学習
lda = LdaModel(corpus, num_topics=4, id2word=dictionary)

# log_perplexity を出力
print(lda.log_perplexity(corpus))
# -2.173078593289852

出力が $-2.17\dots$です。
正常に学習できていれば、Perplexityは約5になるはずなので、$\log(5)=1.609\dots$が期待されるのに、符号から違います。

ドキュメントをよく読んでみましょう。
log_perplexity

Calculate and return per-word likelihood bound, using a chunk of documents as evaluation corpus.
Also output the calculated statistics, including the perplexity=2^(-bound), to log at INFO level.

これによると、perplexityは$2^{-bound}$だと言うことになっていて、どうやら、
log_perplexity()が返しているのは、boundに相当するようです。

計算してみましょう。


print(2**(-lda.log_perplexity(corpus)))
# 4.509847333880428

正解は5なので、それらしい結果が出ています。
ですがしかし、Perplexityとしてはこの値は良すぎます。
今回のダミーデータで学習している限りは5単語未満に絞り込めるはずがないのです。

実際、モデルが学習した結果を見てみましょう。


print(lda.show_topics(num_words=6))
"""
[
    (0, '0.100*"bird" + 0.098*"dog" + 0.092*"melon" + 0.092*"cat" + 0.089*"orange" + 0.089*"rabbit"'), 
    (1, '0.104*"red" + 0.104*"green" + 0.102*"white" + 0.098*"blue" + 0.092*"black" + 0.084*"fish"'),
    (2, '0.136*"lemon" + 0.134*"apple" + 0.128*"banana" + 0.117*"orange" + 0.116*"melon" + 0.045*"China"'),
    (3, '0.216*"France" + 0.191*"America" + 0.181*"Japan" + 0.172*"England" + 0.163*"China" + 0.011*"apple"')
]
"""

本来は各トピック上位の5単語を0.2ずつの出現確率と予測できていないといけないので、今 Perplexity を計算しているモデルは
そんなに精度が良くないのです。(ハイパーパラメーターのチューニングを何もしてないので。それはそれで問題ですが、今回の議題からは外します。)

おかしいので、ソースコードを眺めてみたのですが、 2を底とする対数を取ってる様子は無く、普通に自然対数が使われていました。
なので、これはドキュメントの誤りとみた方が良さそうです。(将来的に修正されると思います。)

perplexity=e^(-bound)

と考えると、辻褄があいます。


print(np.exp(-lda.log_perplexity(corpus)))
# 8.785288789149925

トピック数を 1〜4 と動かして算出してみると明らかです。
トピック数が1の時は全く絞り込めていないので元の単語数の約20,
2の時は半分に絞れるので約10,
4の時は、ちゃんと学習できたら正解の5(ただし、デフォルトのハイパーパラメーターだとそこまで成功しないのでもう少し大きい値)
が算出されるはずです。

やってみます。


for i in range(1, 7):
    # トピック数を指定してモデルを学習
    lda = LdaModel(corpus, num_topics=i, id2word=dictionary)

    print(f"トピック数: {i}, Perplexity: {np.exp(-lda.log_perplexity(corpus))}")
"""
トピック数: 1, Perplexity: 20.032145913774283
トピック数: 2, Perplexity: 11.33724134037765
トピック数: 3, Perplexity: 8.921203895821304
トピック数: 4, Perplexity: 7.436279264160588
トピック数: 5, Perplexity: 7.558708610631221
トピック数: 6, Perplexity: 5.892976661122544
"""

大体想定通りの結果は出ましたね。

さて、 log_perplexity は perplexity の対数では無く、
perplexity 対数の符号を反転させたものである、と言うのは認識しておかないと大間違いの元になります。

というのも、perplexityは小さい方が良いとされる指標です。
と考えると、log_perplexityの出力が小さいパラメーターを選んでしまいがちですがそれは誤りであることがわかります。
対数を取ってから符号を反転させているので、大きいものを採用しないといけないのですね。

(この他、必ずしもPerplexityが小さければいいモデルだとは言えない、と言う議論もあるのですが、
今日の記事の範囲は超えますので省略します。)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です