前回の記事でgensimが登場したので、今度はgensimでトピックモデル(LDA)を実装する方法を紹介します。
ちなみに、僕はLDAをやるときはscikit-learnの方を使うことがほどんどで、gensimのldamodelには慣れていないのでご了承ください。
参考: pythonでトピックモデル(LDA)
gensimの中でもword2vecに比べて若干癖があり、使いにくいように感じています。
早速ですがデータの準備からやっていきます。
使うデータは以前作成したライブドアニュースコーパスのテキストです。
以下の前処理を施しました
– ユニコード正規化
– 分かち書き
– 活用形を原型に戻す
– 名詞,動詞,形容詞のみに絞り込む
– ひらがなのみで構成された単語を取り除く
– アルファベットの小文字統一
(本当はSTOP WORDの辞書を真面目に作るべきなのですが、横着して品詞と文字種だけで絞り込んでいます。)
import pandas as pd
import MeCab
import re
# データの読みこみ
df = pd.read_csv("./livedoor_news_corpus.csv")
# ユニコード正規化
df["text"] = df["text"].str.normalize("NFKC")
# アルファベットを小文字に統一
df["text"] = df["text"].str.lower()
# 分かち書きの中で使うオブジェクト生成
tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
# ひらがなのみの文字列にマッチする正規表現
kana_re = re.compile("^[ぁ-ゖ]+$")
def mecab_tokenizer(text):
# テキストを分かち書きする関数を準備する
parsed_lines = tagger.parse(text).split("\n")[:-2]
surfaces = [l.split('\t')[0] for l in parsed_lines]
features = [l.split('\t')[1] for l in parsed_lines]
# 原型を取得
bases = [f.split(',')[6] for f in features]
# 品詞を取得
pos = [f.split(',')[0] for f in features]
# 各単語を原型に変換する
token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
# 名詞,動詞,形容詞のみに絞り込み
target_pos = ["名詞", "動詞", "形容詞"]
token_list = [t for t, p in zip(token_list, pos) if p in target_pos]
# アルファベットを小文字に統一
token_list = [t.lower() for t in token_list]
# ひらがなのみの単語を除く
token_list = [t for t in token_list if not kana_re.match(t)]
return token_list
# 分かち書きしたデータを作成する
sentences = df.text.apply(mecab_tokenizer)
print(sentences[:5])
"""
0 [2005年, 11月, 2006年, 7月, 読売新聞, 連載, 直木賞, 作家, 角田光...
1 [アンテナ, 張る, 生活, 2月28日, 映画, おかえり、はやぶさ, 3月10日, 公開...
2 [3月2日, 全国ロードショー, スティーブン・スピルバーグ, 待望, 監督, 最新作, 戦...
3 [女優, 香里奈, 18日, 都内, 行う, 映画, ガール, 5月26日, 公開, 女子高...
4 [5日, 東京都千代田区, 内幸町, ホール, 映画, キャプテン・アメリカ/ザ・ファースト...
Name: text, dtype: object
"""
さて、ここからが本番です。
公式ドキュメントのサンプルコードを真似しながら進めます。
models.ldamodel – Latent Dirichlet Allocation
word2vecの時は、分かち書きした単語を配列形式でそのまま取り込んで学習してくれましたが、
LdaModel では各テキストを (単語ID, 出現回数) のタプルの配列に変換しておく必要があります。
Dictionary という専用の関数を用意してくれているのでそれを使います。
from gensim.corpora.dictionary import Dictionary
# 単語と単語IDを対応させる辞書の作成
dictionary = Dictionary(sentences)
# LdaModelが読み込めるBoW形式に変換
corpus = [dictionary.doc2bow(text) for text in sentences]
# 5000番目のテキストを変換した結果。(長いので10単語で打ち切って表示)
print(corpus[5000][:10])
# [(10, 1), (67, 1), (119, 1), (125, 1), (174, 1), (182, 1), (223, 1), (270, 1), (299, 1), (345, 1)]
単語IDと元の単語は以下のようにして変換できます。
# idから単語を取得
print(dictionary[119])
# print(dictionary.id2token[119]) # これも同じ結果
# 復帰
# 単語からidを取得
print(dictionary.token2id["復帰"])
# 119
さて、データができたので学習です。これは非常に簡単でトピックス数を指定して
LdaModelに先ほどのデータと一緒に渡すだけ。
(トピック数は本当はいろいろ試して評価して決める必要があるのですが、今回は元のコーパスが9種類のニュースなので、そのまま9にしました。)
from gensim.models import LdaModel
# トピック数を指定してモデルを学習
lda = LdaModel(corpus, num_topics=9)
学習したモデルを使って、テキストをトピックスに変換するのは次のようにやります。
print(lda[corpus[0]])
# [(0, 0.15036948), (2, 0.81322604), (6, 0.03397929)]
この形式だと個人的には使いにくいと感じているので、
次ようなコードで、DataFrameに変換しています。
(これはもっとクレバーな書き方があると思うので検討中です。)
topic_df = pd.DataFrame(index=range(len(corpus)))
for c in range(9):
topic_df[c] = 0.0
for i in range(len(corpus)):
topics = lda[corpus[i]]
for t, p in topics:
topic_df.loc[i][t] = p
print(topic_df.head().round(3))
"""
0 1 2 3 4 5 6 7 8
0 0.150 0.0 0.813 0.000 0.000 0.000 0.034 0.0 0.000
1 0.000 0.0 0.492 0.000 0.226 0.041 0.000 0.0 0.239
2 0.427 0.0 0.297 0.000 0.052 0.223 0.000 0.0 0.000
3 0.174 0.0 0.543 0.027 0.000 0.253 0.000 0.0 0.000
4 0.000 0.0 0.245 0.000 0.224 0.120 0.000 0.0 0.408
"""
元のカテゴリーとTopicの対応も確認しておきましょう。
ざっと見た限りではうっすらと傾向は出ていますが、そんなに綺麗に分類できている訳ではないですね。
カテゴリ数9をそのまま使ったのは適当すぎました。
main_topic = topic_df.values.argmax(axis=1)
print(pd.crosstab(df.category, main_topic))
"""
col_0 0 1 2 3 4 5 6 7 8
category
dokujo-tsushin 60 3 734 5 13 30 18 5 2
it-life-hack 11 350 35 80 76 29 42 113 134
kaden-channel 11 320 106 32 13 35 208 129 10
livedoor-homme 35 49 168 42 129 25 21 14 28
movie-enter 87 1 93 5 59 377 72 0 176
peachy 130 17 228 162 40 163 86 5 11
smax 2 520 3 87 6 5 2 241 4
sports-watch 29 0 305 1 306 238 19 0 2
topic-news 34 15 200 1 69 340 101 1 9
"""
さて、最後にトピックを構成する単語を見ておきましょう。
独女通信が多く含まれる 2番のトピックでやってみます。
次の関数で、トピックごとの出現頻度上位の単語のIDとその確率が取得できます。
lda.get_topic_terms([topicのid], topn=[取得する個数])
IDだとわかりにくいので、単語に戻して表示しましょう。
for i, prob in lda.get_topic_terms(2, topn=20):
print(i, dictionary.id2token[int(i)], round(prob, 3))
"""
354 思う 0.013
275 人 0.011
178 自分 0.009
883 女性 0.008
186 言う 0.007
2107 結婚 0.007
1211 私 0.007
2833 男性 0.006
1193 多い 0.006
113 彼 0.005
856 仕事 0.005
527 今 0.005
382 気 0.004
162 相手 0.004
183 見る 0.004
270 中 0.004
95 女 0.004
287 何 0.004
614 方 0.004
371 時 0.004
"""
それっぽいのが出てきましたね、