LDA (Latent Dirichlet Allocation) を使って英文記事をクラスタリングする（分類編）

[前回（前処理編）](https://qiita.com/benyamerumo/items/ee68b14e7cdce4c3d628) の続きです．

前回は前処理したドキュメント（ニュース見出し）を processed_docs として扱えるようにしておきました．最初の10件を確認してみます．

```python:
processed_docs[:10]
```

**0               [decid, commun, broadcast, licenc]**
**1                               [wit, awar, defam]**
**2           [call, infrastructur, protect, summit]**
**3                      [staff, aust, strike, rise]**
**4             [strike, affect, australian, travel]**
**5               [ambiti, olsson, win, tripl, jump]**
**6           [antic, delight, record, break, barca]**
**7    [aussi, qualifi, stosur, wast, memphi, match]**
**8            [aust, address, secur, council, iraq]**
**9                         [australia, lock, timet]**
**Name: headline_text, dtype: object**

ここまでで，前処理は終わりです．次に，処理したデータを加工して LDA モデルに与える入力データを作っていきます．

### Bag of Words (BOW)
BOW では，文脈によらず単語が出現したかどうかだけによってベクトルを生成します．
まずはじめに，前処理したドキュメントに表れる単語を全て取り出します．

```python:
dictionary = gensim.corpora.Dictionary(processed_docs)

# dictionary の中身の確認（最初の10個）
count = 0
for k, v, in dictionary.iteritems():
  print(k, v)
  count += 1
  if count > 10:
    break
```

続いて，この辞書（dictionary）にフィルターをかけて重要度があまり高くないと考えられる単語を落としていきます．

```python:
dictionary.filter_extremes(no_below=15, no_above=0.5, keep_n=100000)
```
###### パラメータの説明 [[*]](https://qiita.com/tatsuya-miyamoto/items/f505dfa8d5307f8c6e98)
- no_below (int, optional): 「出現文書数≥指定値」になるような単語のみを保持します．同一文書内の出現数はカウントしません．
- no_above (float, optional): 「出現文書数/全文書数≤指定値」になるような単語のみを保持します．同一文書内の出現数はカウントしません．
- keep_n (int, optional): 辞書の id の小さい順に指定値個のデータを保持します．

ここでは，「前から100000件」のうち，「出現する文書が15件以上」かつ「全文書数のうち出現する文書数の割合が50%以下」を満たす単語を抽出します．抽出したドキュメントは bow_corpus として扱えるようにしておきます．

```python:
bow_corpus = [dictionary.doc2bow(doc) for doc in processed_docs]

# 確認
bow_corpus[4310]
```
**[(76, 1), (112, 1), (484, 1), (4022, 1)]**

4311番目のドキュメントに含まれる単語の BOW におけるベクトルが表示できました．これが何の単語を表しているのかは，以下のように確認できます．

```python:
bow_doc_4310 = bow_corpus[4310]
for i in range(len(bow_doc_4310)):
  print("Word {} (\"{}\") appears {} time."
        .format(bow_doc_4310[i][0], dictionary[bow_doc_4310[i][0]], bow_doc_4310[i][1]))
```
**Word 76 ("bushfir") appears 1 time.**
**Word 112 ("help") appears 1 time.**
**Word 484 ("rain") appears 1 time.**
**Word 4015 ("dampen") appears 1 time.**

### tf-idf
生成した bow_corpus を用いて，tf-idf を計算します．ここで簡単に tf-idf についておさらいしておきます[[**]](https://dev.classmethod.jp/machine-learning/yoshim_2017ad_tfidf_1-2/)．
#### tf (Term Frequency) 
tf は，単語の出現頻度，すなわち「各文書に置いてその単語がどれくらい出現したのか」を意味します．よく出現する単語は，その文書の特徴を判別するのに有用であるという考え方です．

```math
tf = \frac{文書 A における単語 X の出現頻度}{文書 A における全単語の出現頻度}
```

#### idf (Inverse Document Frequency)
idf は，逆文書頻度，すなわち「レアな単語」なら高い値を，「色々な文書によく出現する単語」なら低い値を示します．珍しい単語は，その文書の特徴を判別するのに有用であるという考え方です．文書数の規模に応じた変動の影響を緩和するために，対数を用います．

```math
idf = log(\frac{全文書数}{単語 X を含む文書数})
```

以上の説明から，

```math
tfidf = (単語の出現頻度) * (各単語のレア度)
```
という解釈ができます．つまり，「その単語がよく出現する」ほど，「その単語がレアである」ほど大きい値を示すことになります．この計算を各文書の各単語ごとに行うことで，文書の特徴を判別しやすくします．

上記を踏まえた上で，各文書の tf-idf を計算します．

```python
from gensim import corpora, models

tfidf = models.TfidfModel(bow_corpus)
corpus_tfidf = tfidf[bow_corpus]

# 確認
from pprint import pprint

pprint(corpus_tfidf[4310])
```
**[(76, 0.44381691583659644),**
**(112, 0.38101902676345106),**
**(484, 0.4372048386236042),**
**(4015, 0.6831566259252941)]**

ここまでで，入力データの作成はおわりです．

## BOW を用いた LDA モデルの生成

BOW コーパスから LDA モデルを生成します．抽出したいトピック数(num_topics)は自由に指定できます．ここでは10個のトピックを抽出してみます．

※その他のパラメータについて
id2word には上で作成した (int, str) の dictionary を指定します．passes についてはよくわかっていないのですが，おそらくコーパスを学習する回数だと思います．ここでは2周を指定します．workers には，CPU の最大コア数-1までの値を指定します．値が大きいほど生成が早く終わるようです（[参照](https://radimrehurek.com/gensim/models/ldamulticore.html#module-gensim.models.ldamulticore)）．よくわからない場合は，「LdaMulticore」を「LdaModel」に変えて，workers のパラメータを消してしまってもモデルが生成できると思います．

```python
# モデル生成
lda_model_bow = gensim.models.LdaMulticore(bow_corpus, num_topics=10, id2word=dictionary, passes=2, workers=2)
#gensim.models.LdaModel.save("lda_model_bow.model")  # モデルを保存する場合
#lda_model_bow = gensim.models.LdaModel.load("lda_model_bow.model")  # モデルを読み込む場合

# トピックの表示
for idx, topic in lda_model_bow.print_topics(-1):
  print('Topic: {} \nWords: {}'.format(idx, topic))
```
**Topic: 0 
Words: 0.025*"world" + 0.022*"adelaid" + 0.018*"market" + 0.014*"power" + 0.013*"record" + 0.013*"price" + 0.013*"share" + 0.013*"trial" + 0.011*"fall" + 0.011*"bank"**
**Topic: 1 
Words: 0.022*"say" + 0.019*"elect" + 0.018*"north" + 0.017*"govern" + 0.011*"labor" + 0.011*"minist" + 0.010*"victoria" + 0.010*"health" + 0.010*"state" + 0.009*"talk"**
**Topic: 2 
Words: 0.029*"charg" + 0.028*"court" + 0.021*"murder" + 0.019*"polic" + 0.018*"face" + 0.016*"year" + 0.015*"jail" + 0.014*"accus" + 0.013*"drug" + 0.013*"child"**
**Topic: 3 
Words: 0.015*"turnbul" + 0.014*"deal" + 0.012*"famili" + 0.011*"centr" + 0.011*"say" + 0.010*"research" + 0.010*"drum" + 0.010*"island" + 0.009*"premier" + 0.009*"christma"**
**Topic: 4 
Words: 0.037*"polic" + 0.022*"perth" + 0.022*"coast" + 0.018*"interview" + 0.018*"miss" + 0.017*"shoot" + 0.014*"investig" + 0.014*"gold" + 0.012*"death" + 0.011*"search"**
**Topic: 5 
Words: 0.025*"kill" + 0.021*"die" + 0.020*"countri" + 0.019*"live" + 0.018*"tasmanian" + 0.015*"crash" + 0.014*"dead" + 0.013*"train" + 0.013*"citi" + 0.009*"media"**
**Topic: 6 
Words: 0.033*"trump" + 0.022*"south" + 0.020*"hous" + 0.018*"council" + 0.017*"plan" + 0.016*"brisban" + 0.015*"donald" + 0.014*"hour" + 0.013*"water" + 0.012*"break"**
**Topic: 7 
Words: 0.015*"rural" + 0.013*"indigen" + 0.012*"work" + 0.012*"fund" + 0.011*"school" + 0.011*"govern" + 0.011*"feder" + 0.011*"return" + 0.011*"busi" + 0.010*"flood"**
**Topic: 8 
Words: 0.027*"sydney" + 0.022*"australia" + 0.018*"final" + 0.016*"rise" + 0.015*"test" + 0.014*"peopl" + 0.013*"lose" + 0.011*"beat" + 0.011*"leader" + 0.011*"game"**
**Topic: 9 
Words: 0.029*"queensland" + 0.016*"win" + 0.012*"chang" + 0.011*"australian" + 0.010*"victorian" + 0.010*"port" + 0.010*"public" + 0.010*"year" + 0.010*"take" + 0.009*"star"**

このように，各トピックの単語とそれに対応する重みを使って，10個の異なるトピックを作ることができました．このあたりはトピックが持つ意味を理解できるようにうまく個数を調整してあげる必要がありそうです．

## tf-idf を用いた LDA モデルの生成
```python
# モデル生成
lda_model_tfidf = gensim.models.LdaMulticore(corpus_tfidf, num_topics=10, id2word=dictionary, passes=2, workers=4)
#gensim.models.LdaModel.save("lda_model_tfidf.model")  # モデル保存
#lda_model_tfidf = gensim.models.LdaModel.load("lda_model_tfidf.model")  # モデル読み込み

# トピックの表示
for idx, topic in lda_model_tfidf.print_topics(-1):
  print('Topic: {} Word: {}'.format(idx, topic))
```

（結果は省略）

## BOW を用いた LDA モデルの評価
入力データの生成に使用したドキュメントの一つ (processed_docs[4310]) を入力として与えてみます．

```python
# ドキュメントの確認
print(processed_docs[4310])

# スコアの表示
for index, score in sorted(lda_model_bow[bow_corpus[4310]], key=lambda tup: -1*tup[1]):
  print("\nScore: {}\t \nTopic: {}".format(score, lda_model_bow.print_topic(index, 10)))
```
**['rain', 'help', 'dampen', 'bushfir']**

**Score: 0.4199907183647156
Topic: 0.025*"world" + 0.022*"adelaid" + 0.018*"market" + 0.014*"power" + 0.013*"record" + 0.013*"price" + 0.013*"share" + 0.013*"trial" + 0.011*"fall" + 0.011*"bank"**

**Score: 0.41998225450515747
Topic: 0.015*"turnbul" + 0.014*"deal" + 0.012*"famili" + 0.011*"centr" + 0.011*"say" + 0.010*"research" + 0.010*"drum" + 0.010*"island" + 0.009*"premier" + 0.009*"christma"**

**Score: 0.02001275308430195
Topic: 0.033*"trump" + 0.022*"south" + 0.020*"hous" + 0.018*"council" + 0.017*"plan" + 0.016*"brisban" + 0.015*"donald" + 0.014*"hour" + 0.013*"water" + 0.012*"break"**

**Score: 0.02000552788376808
Topic: 0.015*"rural" + 0.013*"indigen" + 0.012*"work" + 0.012*"fund" + 0.011*"school" + 0.011*"govern" + 0.011*"feder" + 0.011*"return" + 0.011*"busi" + 0.010*"flood"**

（一部省略）

結果から，processed_docs[4310] は上の2つのトピックに属する確率が高いことがわかります．

## tf-idf を用いた LDA モデルの評価
```python
# スコア表示
for index, score in sorted(lda_model_tfidf[bow_corpus[4310]], key=lambda tup: -1*tup[1]):
  print("\nScore: {}\t \nTopic: {}".format(score, lda_model_tfidf.print_topic(index, 10)))
```

（結果は省略）

## BOW を用いた LDA モデルのテスト
最後に，未知のドキュメント unseen_document を用いて，ドキュメントがどのトピックに属するのか計算してみようと思います．
ドキュメントとして使用するのは，"How a Pentagon deal became an identity crisis for Google" という見出しです．

```python
unseen_document = 'How a Pentagon deal became an identity crisis for Google'

# 前処理 -> BOW ベクトル化
bow_vector = dictionary.doc2bow(preprocess(unseen_document))

# スコア表示
for index, score in sorted(lda_model_bow[bow_vector], key=lambda tup: -1*tup[1]):
  print("Score: {}\t Topic: {}".format(score, lda_model_bow.print_topic(index, 5)))
```
**Score: 0.3499999940395355	 Topic: 0.022*"say" + 0.019*"elect" + 0.018*"north" + 0.017*"govern" + 0.011*"labor"**
**Score: 0.1833333522081375	 Topic: 0.015*"turnbul" + 0.014*"deal" + 0.012*"famili" + 0.011*"centr" + 0.011*"say"**
**Score: 0.18333333730697632	 Topic: 0.025*"world" + 0.022*"adelaid" + 0.018*"market" + 0.014*"power" + 0.013*"record"**
**Score: 0.18333333730697632	 Topic: 0.037*"polic" + 0.022*"perth" + 0.022*"coast" + 0.018*"interview" + 0.018*"miss"**
**Score: 0.01666666753590107	 Topic: 0.029*"charg" + 0.028*"court" + 0.021*"murder" + 0.019*"polic" + 0.018*"face"**
**Score: 0.01666666753590107	 Topic: 0.025*"kill" + 0.021*"die" + 0.020*"countri" + 0.019*"live" + 0.018*"tasmanian"**

（一部省略）

結果から，unseen_document は1つ目のトピックに属する確率が最も高く，次に2～4つ目のトピックに属する確率が高いことがわかりました．
また，processed_doc[4310] の属する確率の高いトピック上位2個が unseen_document の属する確率の高いトピックの2,3番目と一致しているので，やや類似度が高いことが予想できます．

# まとめ
以上が，LDA モデルを使ってトピックを生成し，クラスタリングを行う方法になります．基本的には，このようにして生成したトピックを元に，類似度の高いドキュメントを分類するといったタスクなどができるようになります．
本記事では既存の英文サイトを元に， LDA のアルゴリズムを利用する手順を示すことに焦点を当てて執筆しました．理論的なことについてはまだまだ理解の及ばない点が多々あるので，他の記事を当たっていただければと思います．

再度になりますが，もし内容に間違い等があればご指摘いただけると嬉しいです．

# 参考サイト
- [Topic Modeling and Latent Dirichlet Allocation (LDA) in Python](https://towardsdatascience.com/topic-modeling-and-latent-dirichlet-allocation-in-python-9bf156893c24)
- [susanli2016/NLP-with-Python](https://github.com/susanli2016/NLP-with-Python/blob/master/LDA_news_headlines.ipynb)
- [gensim.models.ldamodel](https://radimrehurek.com/gensim/models/ldamodel.html)
- [gensimのDictionaryの中身を簡単にまとめてみた](https://qiita.com/tatsuya-miyamoto/items/f505dfa8d5307f8c6e98)
- [tf-idfについてざっくりまとめ_理論編](https://dev.classmethod.jp/machine-learning/yoshim_2017ad_tfidf_1-2/)