scikit-learnのLatent Dirichlet Allocation (LDA) のcoherenceを求める

# はじめに
ちゃお…†

まいおり…†

LDA (Latent Dirichlet Allocation)、わたしの好きなモデルです。

しかし、現時点の[scikit-learn](https://scikit-learn.org/stable/) (1.2.2) にはLDAモデルのcoherence (コヒーレンス) を求める関数はありません。

そこで強引に？LDAモデルのcoherenceを求める方法を記します。

# コヒーレンスとは
記述や事実の集合は、それらが互いに支持し合っている場合、首尾一貫している (coherent) と言われます。したがって、首尾一貫した事実の集合は、事実のすべてまたは大部分をカバーする文脈で解釈することができます。

トピックのコヒーレンスを測るとは、トピック内の高得点単語間の意味的類似性の程度を測定することで、単一のトピックをスコアリングすることです。これらの測定は、意味的に解釈可能なトピックと、統計的推論の成果物であるトピックを区別するのに役立ちます。

以下にコヒーレンスの評価方法を記します。

- u_mass
  - 文書の共起カウント、1つ前のセグメンテーション、確証尺度として対数条件付き確率に基づいています。
- c_v
  - スライディングウィンドウを用いた、上位単語の1セットセグメンテーションと、正規化ポイントワイズ相互情報 （normalized pointwise mutual information; NPMI） とコサイン類似度を用いた間接的確証尺度を用いたものです。
- c_uci
  - スライディングウィンドウと、与えられたトップワードの全単語ペアのポイントワイズ相互情報（pointwise mutual information; PMI）に基づいています。
- c_npmi
  - 正規化ポイントワイズ相互情報（NPMI）を使用した c_uci コヒーレンスの拡張版です。

# コヒーレンスを求めるフロー
## モジュールのインポート
足らないものはpipでインストールしましょう。ただしtmtoolkitをインストールするとnumpyやscikit-learnなどのバージョンが下がってしまうことがあるので、その場合はnumpyなどを再インストールしましょう。

```py
from tmtoolkit.topicmod.evaluate import metric_coherence_gensim
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.text import CountVectorizer
```

## 前処理
ここらへんはテキストデータに合わせて処理内容を変えましょう。 (特に `CountVectorizer` の `token_pattern`)
```py
corpus = ["This is a pen", "I have a pen",
          "I like cookies", "Cockies make me happy",
          "Working makes me sad", "I like chocolate chip cookies"]

# スペース区切りの文字列を1単語として扱う
vectorizer = CountVectorizer(token_pattern="\S+")
X = vectorizer.fit_transform(corpus)
```
## トピック数 k ごとに coherence を求める

```py
MAX_K = 6
for k in range(2, MAX_K):
    lda = LatentDirichletAllocation(n_components=k, random_state=0)
    lda.fit(X)
    cluster_labels = np.argmax(lda.fit_transform(X), axis=1)
    silhouette_avg = silhouette_score(X, cluster_labels)
    coherence = metric_coherence_gensim(measure='u_mass', 
                                        top_n=5,
                                        dtm=X,
                                        topic_word_distrib=lda.components_, 
                                        vocab=vectorizer.get_feature_names_out(), 
                                        return_mean=True)
    print(k, coherence, silhouette_avg, lda.score(X), lda.perplexity(X))
```

### metric_coherence_gensim 関数の使い方
以下の tmtoolkit の `metric_coherence_gensim` 関数の説明を日本語訳します。
https://tmtoolkit.readthedocs.io/en/latest/api.html#tmtoolkit.topicmod.evaluate.metric_coherence_gensim

`measure` パラメーターでは以下の評価指標が使えます:

- 'u_mass'
- 'c_v'
- 'c_uci'
- 'c_npmi'

`topic_word_distrib` パラメータには `lda.components_` を渡します。 `vocab` パラメータには `vectorizer.get_feature_names_out()` を渡します。 `top_n` は、トピックごとに最も確率の高い単語を何個選択するかを決めるパラメータです。

`measure` に 'u_mass' を選んだ場合は、`dtm` (document-term-matrix) パラメータに scikit-learn のベクタライザーで transform した`X`を渡します。`measure` にそれ以外を選んだ場合は、 `texts` パラメータに以下のような 2D リストを渡します。
```py
[['some', 'text', ...],          # doc. 1
 ['some', 'more', ...],          # doc. 2
 ['another', 'document', ...]]   # doc. 3
```
つまり、'u_mass' 以外を選んだ場合はLDAモデルを作ったときと別のテキストデータが必要になります。

`return_mean` パラメータに True を渡した場合はコヒーレンス値の平均を返します。 False の場合は各トピックごとのコヒーレンス値のリストを返します。

## その他のLDAの評価尺度
他にも近似対数尤度をスコアとして算出する`lda.score()`や、データXの近似的なパープレキシティを計算する`lda.perplexity()`、そしてクラスタ (トピック) 内の凝集度と別クラスタからの乖離度を加味したシルエット係数によって評価することができます。

全部を加えたよくばりコードを下記に記します。ハイパーパラメータチューニングにご活用ください。

```py
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.metrics import silhouette_score
from tmtoolkit.topicmod.evaluate import metric_coherence_gensim

MAX_K = 6
for k in range(2, MAX_K):
    lda = LatentDirichletAllocation(n_components=k, random_state=0)
    cluster_labels = np.argmax(lda.fit_transform(X), axis=1)
    silhouette_avg = silhouette_score(X, cluster_labels)
    coherence = metric_coherence_gensim(measure='u_mass', 
                                        top_n=5,
                                        dtm=X,
                                        topic_word_distrib=lda.components_, 
                                        vocab=vectorizer.get_feature_names_out(), 
                                        return_mean=True)
    print(k, coherence, silhouette_avg, lda.score(X), lda.perplexity(X))
```

# 余談: トピックモデルの可視化
トピックモデルの可視化には[pyLDAvis](https://github.com/bmabey/pyLDAvis)というライブラリが有用です。使い方は、[pyLDAvisを用いたトピックモデル(LDA)の可視化 (scikit-learn編)](https://analytics-note.xyz/machine-learning/pyldavis-sklearn/)を参考にしてください。