【Python】トピックモデル（LDA）

# トピックモデルとは？
最近、自然言語処理の分野はディープラーニング一色ですが、古典的[^1]な手法がまだ使われることもあります。
その古典的な手法の一つにトピックモデルというものがあります。

トピックモデルを簡単に説明すると、確率モデルの一種で、テキストデータ（例：ニュース記事、口コミ）のクラスタリングでよく使われるモデルです。
クラスタリングといえばk平均法（k-means法）が有名ですが、トピックモデルはk平均法とは異なるモデル（アルゴリズム）です。

具体的には、下記のように複数のクラスタに属することを許すのか、許さないかのかが違います。

- k平均法
    - データは一つのクラスタのみに属する
- トピックモデル
    - データは複数のクラスタに属する

例えば、「テニスプレイヤーの大坂なおみ選手が日産自動車から『GT-R』というスポーツカーを寄贈された。」という[ニュース](https://newsroom.nissan-global.com/collections/collection-860852d7040eed420ffbaebb22376714-naomi-osaka_nissan-gt-r_nismo_j)が先日ありました。
大坂なおみ選手の観点から見るとスポーツジャンルのニュースになりますが、日産自動車の観点から見ると経済ジャンルのニュースになります。
k平均法ではスポーツジャンルか経済ジャンルのどちらか一方だけに属することになりますが、トピックモデルではスポーツジャンルと経済ジャンルの両方に属することになります。

このように、複数ジャンルに属しそうなテキストデータが多い場合、k平均法よりもトピックモデルを使った方が良いかもしれません。

# トピックモデルとLDA
LDAは「Latent Dirichlet Allocation」の略称で、日本語では「潜在的ディリクレ配分法」と言います。
そして、「トピックモデル＝LDA」と思われている方がいらっしゃいますが、厳密には両者は異なるものです。

具体的には、LDAはあくまでもトピックモデルの実装の一つであり、トピックモデルの実装にはLDA以外にもPLSA(Probabilistic Latent Semantic Analysis)などがあります。
ただ、LDAが一番有名な実装のため、トピックモデルと言えばLDAという状況になっている次第です。

# トピックモデルの使い方
ここでは「トピックモデル=LDA」という前提のもと、トピックモデルの使い方を説明します。

Pythonのgensimの中に[LDAのライブラリ](https://radimrehurek.com/gensim/models/ldamodel.html)があるので、これを使えば手軽にトピックモデルを試すことができます。
事前に用意するのは、一つのテキストデータを一行とした`train.txt`と`test.txt`のみです。

クラスタリングは教師なし学習なのでトレーニンデータとテストデータに分ける必要はないのですが、[公式サイト](https://radimrehurek.com/gensim/models/ldamodel.html)ではトレーニンデータとテストデータに分けていたのでそれに倣いました。
あと、適当なテキストデータがないためヤフーのトピックス一覧から持ってきました。実際に手元で試す際は分析したいテキストデータに置き換えて下さい。

``` train.txt
日本防衛 長射程重視に変質も
合区で活用 参院特定枠って
21議員の政治資金 使途不透明
公害で死んだ娘 語り継ぐ教訓
「隠れた被害者」加害者の子
ふるさと納税 寄付が広告費に
全国に外国人相談窓口 通訳も
ぐずりにスマホ 非難に悩む親
```

``` test.txt
爆発 現場で缶100本ガス抜き
爆発の瞬間 デマ動画が拡散
バニラエア 来年10月運航終了
コンビニごみ 店舗負担の矛盾
謎めいたカエル 南米で再発見
イニエスタ うつ報道の難しさ
ムネリン笑顔「少し元気に」
G菅野 ゴジラ超え6.5億円に
```

`train.txt`と`test.txt`が用意できれば、あとはプログラムを十数行書くだけです。
トピックス数だけは事前に決める必要があり、クラスタリング結果を見ながら調整していくと良いでしょう。

``` python
import MeCab
from gensim.corpora.dictionary import Dictionary
from gensim.models import LdaModel
from collections import defaultdict

# MeCabオブジェクトの生成
mt = MeCab.Tagger('')
mt.parse('')

# トピック数の設定
NUM_TOPICS = 3

if __name__ == "__main__":
    # トレーニングデータの読み込み
    # train_texts は二次元のリスト
    # テキストデータを一件ずつ分かち書き（名詞、動詞、形容詞に限定）して train_texts に格納するだけ
    train_texts = []
    with open('./train.txt', 'r') as f:
        for line in f:
            text = []
            node = mt.parseToNode(line.strip())
            while node:
                fields = node.feature.split(",")
                if fields[0] == '名詞' or fields[0] == '動詞' or fields[0] == '形容詞':
                    text.append(node.surface)
                node = node.next
            train_texts.append(text)

    # モデル作成
    dictionary = Dictionary(train_texts)
    corpus = [dictionary.doc2bow(text) for text in train_texts]
    lda = LdaModel(corpus=corpus, num_topics=NUM_TOPICS, id2word=dictionary)

    # テストデータ読み込み
    # test_texts は train_texts と同じフォーマット
    test_texts = []
    raw_test_texts = []
    with open('./test.txt', 'r') as f:
        for line in f:
            text = []
            raw_test_texts.append(line.strip())
            node = mt.parseToNode(line.strip())
            while node:
                fields = node.feature.split(",")
                if fields[0] == '名詞' or fields[0] == '動詞' or fields[0] == '形容詞':
                    text.append(node.surface)
                node = node.next
            test_texts.append(text)

    # テストデータをモデルに掛ける
    score_by_topic = defaultdict(int)
    test_corpus = [dictionary.doc2bow(text) for text in test_texts]

    # クラスタリング結果を出力
    for unseen_doc, raw_train_text in zip(test_corpus, raw_test_texts):
        print(raw_train_text, end='\t')
        for topic, score in lda[unseen_doc]:
            score_by_topic[int(topic)] = float(score)
        for i in range(NUM_TOPICS):
            print('{:.2f}'.format(score_by_topic[i]), end='\t')
        print()
```

このプログラムを動かすと次のような結果が出力されます。
トピック数は3に設定したので、2列目がトピック1、3列目がトピック2、4列目がトピック3の確率になります。
今回はデータ数が少ないので、どのデータも確率がほぼ同じになっています。

```
爆発 現場で缶100本ガス抜き　　　　　0.33　　　　0.33　　　　0.33	
爆発の瞬間 デマ動画が拡散　　　　　　　0.33　　　　0.33　　　　0.33	
バニラエア 来年10月運航終了　　　　0.33　　　　0.33　　　　0.33	
コンビニごみ 店舗負担の矛盾　　　　　　0.33　　　　0.33　　　　0.33	
謎めいたカエル 南米で再発見　　　　　0.33　　　　0.33　　　　0.33	
イニエスタ うつ報道の難しさ　　　　　　　　0.33　　　　0.33　　　　0.33	
ムネリン笑顔「少し元気に」　　　　　　　　　　　　0.33　　　　0.33　　　　0.33	
G菅野 ゴジラ超え6.5億円に　　　　　　　0.66　　　　0.17　　　　0.17	
```

# 最後に
この記事を読んで、トピックモデルの概要とトピックモデルを手軽に利用できることが分かって頂けると嬉しいです。
トピックモデルの理論について詳しく知りたい方は、ネット上にたくさん解説記事がありますので是非読んでみて下さい。

[^1]: ディープラーニングが流行り始めた時期よりも前という意味で