22. トピックモデルによるクラスタリング

トピックモデルとは文書中の単語出現分布を元に傾向(≒トピックらしきもの)を観察しようとするアプローチで、クラスタリングの一種である。なお、一般的なクラスタリング(例えばk平均法)では一つのサンプルが一つのクラスタに属するという前提でグルーピングを行うのに対し、トピックモデルでは一つのサンプルが複数のクラスタを内包しているという前提でグルーピングを行う。次の例を眺めるとイメージをつかみやすいだろう。

基本的には文書を BoW (CountVectrizor) やそれの重みを調整した TF-IDF 等の「文書単語行列」を作成し、ここから文書館類似度や単語間類似度を元に集約(≒次元削減)を試みる。文書単語行列の作成方法や次元削減方法、類似度の求め方などで様々なアルゴリズムが提案されている。ここでは (1) Bow + LDA によりトピックモデルを行い、PyLDAvisによる可視化を通してトピックを観察してみよう。

なお、トピックモデルの注意点として、トピックそのものは人手による解釈が求められる点が挙げられる。例えば先に上げたトピックモデル入門:WikipediaをLDAモデル化してみたにおける図2(下図)では「政治」「スポーツ」「国際」といったトピックが並んでいるが、実際には「4-1. トピック観察」を行う必要がある。実際に観察してみよう。

22.1. データの準備

これまで見てきたいつものやつ。

!curl -O https://ie.u-ryukyu.ac.jp/~tnal/2022/dm/static/r_assesment.pkl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 34834  100 34834    0     0   100k      0 --:--:-- --:--:-- --:--:--  103k
import collections

import numpy as np
import pandas as pd
import spacy
from wordcloud import WordCloud

nlp = spacy.load("ja_ginza")

assesment_df = pd.read_pickle('r_assesment.pkl')
assesment_df.head()
title grade required q_id comment
0 工業数学Ⅰ 1 True Q21 (1) 特になし
1 工業数学Ⅰ 1 True Q21 (2) 正直わかりずらい。むだに間があるし。
2 工業数学Ⅰ 1 True Q21 (2) 例題を取り入れて理解しやすくしてほしい。
3 工業数学Ⅰ 1 True Q21 (2) 特になし
4 工業数学Ⅰ 1 True Q21 (2) スライドに書く文字をもう少しわかりやすくして欲しいです。
# 分かち書き
poses = ['PROPN', 'NOUN', 'VERB', 'ADJ', 'ADV'] #名詞、動詞、形容詞、形容動詞

assesment_df['wakati'] = ''
for index, comment in enumerate(assesment_df['comment']):
    doc = nlp(comment)
    wakati_words = []
    for token in doc:
        if token.pos_ in poses:
            wakati_words.append(token.lemma_)
    wakati_text = ' '.join(wakati_words)
    assesment_df.at[index, 'wakati'] = wakati_text

assesment_df
title grade required q_id comment wakati
0 工業数学Ⅰ 1 True Q21 (1) 特になし 特に なし
1 工業数学Ⅰ 1 True Q21 (2) 正直わかりずらい。むだに間があるし。 正直 わかる ずらい むだ 間 ある
2 工業数学Ⅰ 1 True Q21 (2) 例題を取り入れて理解しやすくしてほしい。 例題 取り入れる 理解 する
3 工業数学Ⅰ 1 True Q21 (2) 特になし 特に なし
4 工業数学Ⅰ 1 True Q21 (2) スライドに書く文字をもう少しわかりやすくして欲しいです。 スライド 書く 文字 もう 少し わかる する
... ... ... ... ... ... ...
165 データマイニング 3 False Q22 課題が難しいものが多く、時間を多くとってもらえたのは非常に良かったですがかなりきつかったです... 課題 難しい もの 多い 時間 多い とる もらえる 非常 良い かなり きつい ござる
166 ICT実践英語Ⅰ 3 False Q22 オンラインなどで顔を合わせてやりたかったです。 オンライン 顔 合わせる やる
167 知能情報実験Ⅲ 3 True Q21 (2) unityの操作方法の説明などを最初に行ってもらえたらもう少しスムーズにできたのではないかと思う。 unity 操作方法 説明 最初 行く もらえる もう 少し スムーズ できる 思う
168 知能情報実験Ⅲ 3 True Q22 それぞれに任せるといった形で進められたものだったのでそれなりに進めやすかったですが、オンライ... それぞれ 任せる いう 形 進める もの なり 進める オンライン 班 員 指導 全く する...
169 知能情報実験Ⅲ 3 True Q22 モバイルアプリ班\r\nHTML/CSS,JavaScriptなどを用いてアプリケーションを... モバイルアプリ 班 \r\n HTML CSS javascript 用いる アプリケーショ...

170 rows × 6 columns

22.2. 文書ベクトルの作成

ここでは CountVectorizer (Bag-of-Words) で作成してみよう。

from sklearn.feature_extraction.text import CountVectorizer

stop_words = ['こと', '\r\n', 'ため', '思う', 'いる', 'ある', 'する', 'なる']
vectorizer = CountVectorizer(stop_words=stop_words)
bow_tf_vector = vectorizer.fit_transform(assesment_df['wakati'])
print('bow_tf_vector.shape = ', bow_tf_vector.shape)
bow_tf_vector.shape =  (170, 738)

22.3. LDAによるトピックモデル解析

sklearnでは LatentDirichletAllocation として用意されている。

from sklearn.decomposition import LatentDirichletAllocation

NUM_TOPICS = 20 #トピック数
max_iter = 100  #LDAによる学習回数
lda = LatentDirichletAllocation(n_components=NUM_TOPICS, max_iter=max_iter, learning_method='online',verbose=True)
data_lda = lda.fit_transform(bow_tf_vector)
iteration: 1 of max_iter: 100
iteration: 2 of max_iter: 100
iteration: 3 of max_iter: 100
iteration: 4 of max_iter: 100
iteration: 5 of max_iter: 100
iteration: 6 of max_iter: 100
iteration: 7 of max_iter: 100
iteration: 8 of max_iter: 100
iteration: 9 of max_iter: 100
iteration: 10 of max_iter: 100
iteration: 11 of max_iter: 100
iteration: 12 of max_iter: 100
iteration: 13 of max_iter: 100
iteration: 14 of max_iter: 100
iteration: 15 of max_iter: 100
iteration: 16 of max_iter: 100
iteration: 17 of max_iter: 100
iteration: 18 of max_iter: 100
iteration: 19 of max_iter: 100
iteration: 20 of max_iter: 100
iteration: 21 of max_iter: 100
iteration: 22 of max_iter: 100
iteration: 23 of max_iter: 100
iteration: 24 of max_iter: 100
iteration: 25 of max_iter: 100
iteration: 26 of max_iter: 100
iteration: 27 of max_iter: 100
iteration: 28 of max_iter: 100
iteration: 29 of max_iter: 100
iteration: 30 of max_iter: 100
iteration: 31 of max_iter: 100
iteration: 32 of max_iter: 100
iteration: 33 of max_iter: 100
iteration: 34 of max_iter: 100
iteration: 35 of max_iter: 100
iteration: 36 of max_iter: 100
iteration: 37 of max_iter: 100
iteration: 38 of max_iter: 100
iteration: 39 of max_iter: 100
iteration: 40 of max_iter: 100
iteration: 41 of max_iter: 100
iteration: 42 of max_iter: 100
iteration: 43 of max_iter: 100
iteration: 44 of max_iter: 100
iteration: 45 of max_iter: 100
iteration: 46 of max_iter: 100
iteration: 47 of max_iter: 100
iteration: 48 of max_iter: 100
iteration: 49 of max_iter: 100
iteration: 50 of max_iter: 100
iteration: 51 of max_iter: 100
iteration: 52 of max_iter: 100
iteration: 53 of max_iter: 100
iteration: 54 of max_iter: 100
iteration: 55 of max_iter: 100
iteration: 56 of max_iter: 100
iteration: 57 of max_iter: 100
iteration: 58 of max_iter: 100
iteration: 59 of max_iter: 100
iteration: 60 of max_iter: 100
iteration: 61 of max_iter: 100
iteration: 62 of max_iter: 100
iteration: 63 of max_iter: 100
iteration: 64 of max_iter: 100
iteration: 65 of max_iter: 100
iteration: 66 of max_iter: 100
iteration: 67 of max_iter: 100
iteration: 68 of max_iter: 100
iteration: 69 of max_iter: 100
iteration: 70 of max_iter: 100
iteration: 71 of max_iter: 100
iteration: 72 of max_iter: 100
iteration: 73 of max_iter: 100
iteration: 74 of max_iter: 100
iteration: 75 of max_iter: 100
iteration: 76 of max_iter: 100
iteration: 77 of max_iter: 100
iteration: 78 of max_iter: 100
iteration: 79 of max_iter: 100
iteration: 80 of max_iter: 100
iteration: 81 of max_iter: 100
iteration: 82 of max_iter: 100
iteration: 83 of max_iter: 100
iteration: 84 of max_iter: 100
iteration: 85 of max_iter: 100
iteration: 86 of max_iter: 100
iteration: 87 of max_iter: 100
iteration: 88 of max_iter: 100
iteration: 89 of max_iter: 100
iteration: 90 of max_iter: 100
iteration: 91 of max_iter: 100
iteration: 92 of max_iter: 100
iteration: 93 of max_iter: 100
iteration: 94 of max_iter: 100
iteration: 95 of max_iter: 100
iteration: 96 of max_iter: 100
iteration: 97 of max_iter: 100
iteration: 98 of max_iter: 100
iteration: 99 of max_iter: 100
iteration: 100 of max_iter: 100

22.4. トピックの観察。

  • pyLDAvisによりトピックを観察してみよう。

  • 下図の左側がトピック分布を表している。丸の大きさがトピック内に含まれる文書数、丸と丸の距離はトピック間の距離。

  • 下図の右側が単語の発生頻度を表している。

    • トピックを選択するとそのトピックにおける単語の発生頻度を観察できる。

    • 単語を選択すると、その単語がどのようにトピック分布上にバラけているかを観察できる。

import pyLDAvis
from pyLDAvis import sklearn as sklearn_vis

pyLDAvis.enable_notebook()
dash = sklearn_vis.prepare(lda, bow_tf_vector, vectorizer, mds='tsne')
dash
/Users/tnal/.venv/dm/lib/python3.8/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  from imp import reload
/Users/tnal/.venv/dm/lib/python3.8/site-packages/sklearn/utils/deprecation.py:87: FutureWarning: Function get_feature_names is deprecated; get_feature_names is deprecated in 1.0 and will be removed in 1.2. Please use get_feature_names_out instead.
  warnings.warn(msg, category=FutureWarning)
/Users/tnal/.venv/dm/lib/python3.8/site-packages/pyLDAvis/_prepare.py:246: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only.
  default_term_info = default_term_info.sort_values(
/Users/tnal/.venv/dm/lib/python3.8/site-packages/sklearn/manifold/_t_sne.py:780: FutureWarning: The default initialization in TSNE will change from 'random' to 'pca' in 1.2.
  warnings.warn(
/Users/tnal/.venv/dm/lib/python3.8/site-packages/sklearn/manifold/_t_sne.py:790: FutureWarning: The default learning rate in TSNE will change from 200.0 to 'auto' in 1.2.
  warnings.warn(
/Users/tnal/.venv/dm/lib/python3.8/site-packages/sklearn/manifold/_t_sne.py:819: FutureWarning: 'square_distances' has been introduced in 0.24 to help phase out legacy squaring behavior. The 'legacy' setting will be removed in 1.1 (renaming of 0.26), and the default setting will be changed to True. In 1.3, 'square_distances' will be removed altogether, and distances will be squared by default. Set 'square_distances'=True to silence this warning.
  warnings.warn(