gensimのLDA評価指標coherenceの使い方

LDAを使う機会があり、その中でトピックモデルの評価指標の一つであるcoherenceについて調べたのでそのまとめです。理論的な内容というより、gensimを用いてLDAを計算した際の使い方がメイン~~です~~のつもりでした。
【追記　2018/6/20】コードを見直したところ、割と酷かったので修正しました。
# 環境
* Python 3.6.5
* Mac 10.13.4

# coherenceとは
そもそもcoherenceって何？という方向けに軽い解説です。
まず、LDAに代表されるトピックモデルでは、文章のトピックというものを算出することができます。例えばニュース記事であれば、政治トピックや芸能トピックにスポーツトピックなどが存在するでしょう。そしてトピックモデルに文書群（この場合はニュース記事群）と合計トピック数をぶん投げると、自動でそれっぽいトピック（政治や芸能）を推測し、各文書の所属確率を計算してくれます（だいぶ雑な説明です）。超便利ですね。
が、ここで問題となることが一つ。トピックモデルの推測したトピックというのは、あくまで勝手にトピックモデル側がふぁんたじっくな方法によって決定したものです。コレ本当にちゃんとしたトピックになってんの？という疑問の答えは、そのままでは手に入りません。そこで出てきたのが、perplexityやcoherenceといった評価指標です。

* perplexity: モデルの予測精度の評価指標
* coherence: トピックの品質を測る評価指標

perplexityに関しては、トピックモデルによる予測精度というそのまんまの評価指標です。単純明快でわかりやすい……かと思いきや、調べたら余計こんがらがりました。とりあえず予測精度を測っています。
そしてcoherenceで測るトピックの品質ですが、こちらは人間がいかに理解しやすいトピックであるかを表しています。トピック内に含まれる語に一貫性があり、人の目でぱっと見しただけで何トピックかがわかると良いわけです。
このcoherenceの算出には、いくつかの手法が存在しています。ここではひとまずgensimのcoherence計算として採用されているものを挙げます。それぞれの詳しい説明に関しては、ここでは省略します。最後のc_vに関する論文では、ここで挙げられている全ての手法の比較実験なども行っているのでそちらをご参照ください。

* c_uci 論文:[Automatic Evaluation of Topic Coherence](http://www.aclweb.org/anthology/N10-1012) 著者:Newman, Lau, Grieser, Baldwin
* u_mass 論文:[Optimizing Semantic Coherence in Topic Models](https://mimno.infosci.cornell.edu/papers/mimno-semantic-emnlp.pdf) 論文著者:Mimno, Wallach, Talley, Leenders, McCallum
* c_npmi 論文:[Evaluating Topic Coherence Using Distributional Semantics](http://www.aclweb.org/anthology/W13-0102)著者:Aletras, Stevenson
* c_v 論文:[Exploring the Space of Topic Coherence Measures](http://svn.aksw.org/papers/2015/WSDM_Topic_Evaluation/public.pdf)著者:Röder, Both, Hinneburg

さて、それぞれの手法の中身はさておき、gensimで計算する上でどれがいいの？という疑問があります。結論から言えば__精度ならc_v__、__手軽さならu_mass__です。最も精度が高いとされるc_vでは、coherence計算のためにLDAの学習に用いたデータとは異なるテキストが必要です。また、実行時間が長くかかるという欠点もあります。それに対しu_massでは、LDAの学習で用いたコーパスをそのまま利用することができ、また実行速度もc_vに比べ圧倒的に高速です。c_npmiとc_uciはこの二つの中間の性能です（どちらも学習とは別のテキストが必要）。
実際の実行時間に関しては、次で説明します。

# 使い方

```python:lda_coherence.py
# c_v,c_uci, c_npmi, u_massの実行

import gensim
import time

dictfile='./dict/emo_dictionary.txt'
dictionary=gensim.corpora.Dictionary.load_from_text(dictfile) # 辞書読み込み

corpdir='./corpus/emo_mini/emo_{}.mm'
ldafile='./model_file/lda_test_time.model'
num_topics = 5 # トピック数5
print('lda学習')
for num in [0,5000,10000,15000,20000,25000,30000,35000,40000,45000]:
    print(corpdir.format(num))
    corpus = gensim.corpora.MmCorpus(corpdir.format(num))
    if num == 0:
        lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=num_topics, id2word=dictionary)
    else:
        lda.update(corpus=corpus)

# c_v, c_uci, c_npmi用に、学習に用いたコーパスとは別のコーパスからtextを用意
texts = []
corpus = gensim.corpora.MmCorpus(corpdir.format(50000))
for doc in corpus:
    text = []
    for word in doc:
        for i in range(int(word[1])):
            text.append(dictionary[word[0]])
    texts.append(text)

print('\ncoherence計算')
# c_v計算
start = time.time()
cm = gensim.models.CoherenceModel(lda,texts=texts,dictionary=dictionary,coherence='c_v')
print('c_v coherence score: {}'.format(cm.get_coherence()))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]\n")

# c_uci計算
start = time.time()
cm = gensim.models.CoherenceModel(lda,texts=texts,dictionary=dictionary,coherence='c_uci')
print('c_uci coherence score: {}'.format(cm.get_coherence()))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]\n")

# c_npmi計算
start = time.time()
cm = gensim.models.CoherenceModel(lda,texts=texts,dictionary=dictionary,coherence='c_npmi')
print('c_npmi coherence score: {}'.format(cm.get_coherence()))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]\n")

# u_mass計算
corpus = gensim.corpora.MmCorpus(corpdir.format(35000)) # u_mass用に学習に用いたコーパスを用意ß

start = time.time()
cm = gensim.models.CoherenceModel(lda,corpus=corpus,dictionary=dictionary,coherence='u_mass')
print('u_mass coherence score: {}'.format(cm.get_coherence()))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]\n")

```

```text:結果
lda学習
./corpus/emo_mini/emo_0.mm
./corpus/emo_mini/emo_5000.mm
./corpus/emo_mini/emo_10000.mm
./corpus/emo_mini/emo_15000.mm
./corpus/emo_mini/emo_20000.mm
./corpus/emo_mini/emo_25000.mm
./corpus/emo_mini/emo_30000.mm
./corpus/emo_mini/emo_35000.mm
./corpus/emo_mini/emo_40000.mm
./corpus/emo_mini/emo_45000.mm

coherence計算
c_v coherence score: 0.4539085826409012
elapsed_time:15.874798774719238[sec]

c_uci coherence score: -3.061872963562876
elapsed_time:8.376861810684204[sec]

c_npmi coherence score: -0.1500405034367553
elapsed_time:8.649868249893188[sec]

u_mass coherence score: -0.5865097336152332
elapsed_time:0.42345118522644043[sec]
```
今回学習に用いたのは、__禁則事項__で集めた書籍ごとにレビューを合算したものから形容詞,形容動詞,副詞を抽出したデータです。予め5000冊単位でコーパス化しており、トピック数5で学習したのち再学習でモデルを更新しています。
c_v,c_uci,c_npmiでは、学習で用いなかった別の5000冊のコーパスを以下のような形式に直して計算に用いています。コーパスのままじゃダメなの？と思ってコーパスに入れて辞書指定したりしてみましたが、以下の形式でない限り意地でもtextがねえぞ！とエラーを吐くようです。頑固すぎか。

```python:gensim公式より
texts = [['system', 'human', 'system', 'eps'],
         ['user', 'response', 'time'],
         ['trees'],
         ['graph', 'trees'],
         ['graph', 'minors', 'trees'],
         ['graph', 'minors', 'survey']]
```

u_massでは、学習の際最初に使った5000冊のコーパスをもう一度突っ込んで計算しています。コーパスによって多少データの量にばらつきはあると思いますが、それでもu_massの計算が圧倒的に早いです。
【追記】今回coherence計算に用いたコーパスは、c_v,c_uci,c_npmiが8.4MB、u_massが8.3MBです。

# top_topics()
さて、上の例ではCoherenceModelを用いてcoherenceを計算しました。しかしgensimのldamodelにも、coherenceを計算するためのtop_topicsという関数が存在します。何が違うの？となりますが、ソースコードを見れば一発でした。単にgensim側が親切にCoherenceModelをわざわざこっちで作らずとも、内部的にCoherenceModelを使って計算してくれるというものでした。
動作の違いは以下のようになります。モデルのトピック数は20です。

```python
import gensim

ldafile='./model_file/lda_emo.model'
corpfile='./corpus/emo_corpus.mm'

lda = gensim.models.LdaModel.load(ldafile)
corpus = gensim.corpora.MmCorpus(corpfile)
cm = gensim.models.CoherenceModel(lda,corpus=corpus,coherence='u_mass')

cohsum=0
for coherence in lda.top_topics(corpus=corpus):
    print(coherence[1])
    cohsum+=coherence[1]
print('topics coherence average:{}'.format(cohsum/20))

print('=================================')

cohsum=0
for coherence in cm.get_coherence_per_topic():
    print(coherence)
print('CoherenceModel coherence:{}'.format(cm.get_coherence()))
```

```text:結果
-0.6393429540404041
-0.7152079740561246
-0.719350861928879
-0.7239273159422894
-0.7251487277519775
-0.7419321700359717
-0.749729380891211
-0.783093386505951
-0.823903285450112
-0.8861784254173724
-0.9014051303372574
-0.9133562021522793
-1.0032725217646268
-1.0178234887587652
-1.077725546796488
-1.0922208158115885
-1.221604842775401
-1.2313407256415705
-1.3177686505983774
-17.8060783223566
topics coherence average:-1.7545205364506622
=================================
-0.9014051303372574
-1.3177686505983774
-0.823903285450112
-1.0922208158115885
-0.783093386505951
-0.8861784254173724
-1.0178234887587652
-0.7152079740561246
-0.719350861928879
-1.221604842775401
-0.6393429540404041
-0.7251487277519775
-0.749729380891211
-0.7419321700359717
-17.8060783223566
-1.077725546796488
-0.9133562021522793
-0.7239273159422894
-1.2313407256415705
-1.0032725217646268
CoherenceModel coherence:-1.7545205364506629
```

top_topicsではトピックごとのcoherenceを計算しており、これはCoherenceModelのget_coherence_per_topicに対応しています。全体のcoherence計算をする場合は、トピックごとのcoherenceの平均を取れば良いです。

# おわりに
coherenceの具体的な内容とかめんどいから、具体的な使い方さえ分かればいいやと思ったのに、割と普通に調べる羽目になりました。ですが雑な部分が多々あるので、間違いがあるかもしれません（特に英語サイトから調べてきたような内容）。なので間違いがあった場合には、どうか容赦無くご指摘ください。
そしてソースコードを眺めいて気づいたのですが、何やらcoherence計算にword2vecを用いる方法があるようです。c_w2vってそういうことよね？ただ公式サイトの方には何もそれらしき記載がなかったので、どうなんだこれといった次第で取り上げていません。~~試すためにword2vec計算するのが面倒だった。~~なのでひとまず今回は保留して今後に期待です。


# 参考サイト
* [gensim: models.ldamodel – Latent Dirichlet Allocation](https://radimrehurek.com/gensim/models/ldamodel.html#gensim.models.ldamodel.LdaModel)
* [gensim: models.coherencemodel – Topic coherence pipeline](https://radimrehurek.com/gensim/models/coherencemodel.html)
* [gensim/ldamodel.py at develop · RaRe-Technologies/gensim](https://github.com/RaRe-Technologies/gensim/blob/develop/gensim/models/ldamodel.py)
* [gensim/coherencemodel.py at develop · RaRe-Technologies/gensim](https://github.com/RaRe-Technologies/gensim/blob/develop/gensim/models/coherencemodel.py)
* [トピックモデルの評価指標 Perplexity とは何なのか？](https://www.slideshare.net/hoxo_m/perplexity)
* [トピックモデルの評価指標 Coherence に関する論文まとめ - ほくそ笑む](http://d.hatena.ne.jp/hoxo_m/20160122/p1)
* [ドキュメント コレクション分析 - Azure | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/machine-learning/desktop-workbench/scenario-document-collection-analysis)
* [論文感想:"Exploring Topic Coherence over many models and many topics"(EMNLP-CoNLL 2012) - 唯物是真 @Scaled_Wurm](http://sucrose.hatenablog.com/entry/2012/07/15/160850)
* [Validating gensim's topic coherence pipeline | RARE Technologies](https://rare-technologies.com/validating-gensims-topic-coherence-pipeline/)
* [【Python】処理にかかる時間を計測して表示 - Qiita](https://qiita.com/fantm21/items/3dc7fbf4e935311488bc)