LDA(Latent Dirichlet allocation)トピックモデルは教師なし学習アルゴリズムで、BOW(Bag-of-Word)モデルの一種です。一つドキュメントは語彙で構成されますが、語彙同士に前後関係がないと仮定します。また、ドキュメントに複数のトピックを含んでいて、ドキュメントの語彙はトピックから生成されるとします。
LDAトピックモデルの詳しい解説はこの記事をご参照ください。
今回の記事では、scikit-learnでLDAトピックモデルを体験してみましょう。以下の順で説明して行きます。
- LDAトピックモデルのデータ準備
- LDAトピックモデル前処理の結果を可視化する
- LDAトピックモデルを構築する
- 各トピックを可視化して理解しよう
- 新しいテキストがどのトピックを話ているかを識別してみよう
日本語の形態素解析はMeCabを使います。
LDAトピックモデルのデータ準備
今回のlivedoorニュースデータを利用します。ダウンロードしたファイルを解凍、dataフォルダーに置きます。では早速データを読み込んでpandasのSeriesを作りましょう。
import pandas as pd
import glob
text_paths = glob.glob('data/text/**/*.txt')
texts = []
for text_path in text_paths:
text = open(text_path, 'r').read()
text = text.split('\n')
title = text[2]
text = ' '.join(text[3:])
texts.append(text)
news_ss = pd.Series(texts)
display(news_ss.head())
0 2005年11月から翌2006年7月まで読売新聞にて連載された、直木賞作家・角田光代による... 1 「アンテナを張りながら生活をしていけばいい」 2月28日、映画『おかえり、はやぶさ』(... 2 3月2日より全国ロードショーとなる、スティーブン・スピルバーグの待望の監督最新作『戦火の馬... 3 女優の香里奈が18日、都内で行われた映画『ガール』(5月26日公開)の女子高生限定試写会に... 4 5日、東京・千代田区の内幸町ホールにて、映画『キャプテン・アメリカ/ザ・ファースト・アベン... dtype: object
Seriesの一行が一記事になっています。
次に、日本語の形態素解析等のNLP前処理を行います。ここは深層学習ライブラリを使わずに素直にMeCabを使います。前処理では以下のことを行います。
- 形態素を解析し、名詞、動詞、形容詞のみを残す
- ストップワードを除外する
- 大文字、小文字を統一する
他にも、単語を基本形に統一する、カタカナと漢字を統一する等必要に応じて前処理をやりましょう。
import MeCab
tagger = MeCab.Tagger("-Ochasen")
import mojimoji
import os
import urllib
def load_jp_stopwords(path="data/jp_stop_words.txt"):
url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
if os.path.exists(path):
print('File already exists.')
else:
print('Downloading...')
urllib.request.urlretrieve(url, path)
return pd.read_csv(path, header=None)[0].tolist()
def preprocess_jp(series):
stop_words = load_jp_stopwords()
def tokenizer_func(text):
tokens = []
node = tagger.parseToNode(str(text))
while node:
features = node.feature.split(',')
surface = features[6]
if (surface == '*') or (len(surface) < 2) or (surface in stop_words):
node = node.next
continue
noun_flag = (features[0] == '名詞')
proper_noun_flag = (features[0] == '名詞') & (features[1] == '固有名詞')
verb_flag = (features[0] == '動詞') & (features[1] == '自立')
adjective_flag = (features[0] == '形容詞') & (features[1] == '自立')
if proper_noun_flag:
tokens.append(surface)
elif noun_flag:
tokens.append(surface)
elif verb_flag:
tokens.append(surface)
elif adjective_flag:
tokens.append(surface)
node = node.next
return " ".join(tokens)
series = series.map(tokenizer_func)
#---------------Normalization-----------#
series = series.map(lambda x: x.lower())
series = series.map(mojimoji.zen_to_han)
return series
processed_news_ss = preprocess_jp(news_ss)
display(processed_news_ss.head())
0 読売新聞 連載 直木賞 作家 角田 光代 長編 サスペンス れい きい 出演 テレビ ト... 1 アンテナ 張る 生活 映画 ぶす 公開 文部 科学 タイアップ 千代田 区立 神田 中学校... 2 全国 ロードショー スティーブン スピルバーグ 待望 監督 最新 戦火 早い アカ... 3 女優 香里奈 都内 行う 映画 ガール 公開 女子高 限定 試写 出席 女子高 質問 答え... 4 東京 千代田 内幸町 ホール 映画 キャプテン アメリカ 公開 記念 宿命 ライバル 対... dtype: object
前処理で各記事がスペース区切りの語彙集合となりました。
LDAトピックモデル前処理の結果を可視化する
LDAトピックモデルを構築する前に、データセットを直感的に理解してみましょう。WordCloudを使いのが手取り早い方法です。
from wordcloud import WordCloud
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
font_path="/Library/Fonts/ipaexg.ttf"
font_property = matplotlib.font_manager.FontProperties(fname=font_path, size=24)
def show_wordcloud(series):
long_string = ','.join(list(series.values))
# Create a WordCloud object
wordcloud = WordCloud(font_path=font_path, background_color="white", max_words=1000, contour_width=3, contour_color='steelblue')
# Generate a word cloud
wordcloud.generate(long_string)
# Visualize the word cloud
plt.imshow(wordcloud)
plt.show()
show_wordcloud(processed_news_ss)

LDAトピックモデルの構築
LDAトピックモデルを学習させるために、教師データ(先ほど前処理したデータ)を記事-語彙(Document-Word)のマトリクスに変換する必要があります。scikit-learnのCounterVectorizerでマトリクスを作れますが、ここではさらにtf-idf(term frequency-inverse document frequency)を適用した記事-語彙(Document-Word)のマトリクスを作ります。以下で(7374記事)X(35384語彙)の記事-語彙(Document-Word)のマトリクスを作りました。
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
count_vectorizer = CountVectorizer()
count_data = count_vectorizer.fit_transform(processed_news_ss)
tfidf_vectorizer = TfidfTransformer()
tfidf_data = tfidf_vectorizer.fit_transform(count_data)
print(tfidf_data.toarray())
(7374, 35384)
では、LDAトピックモデルを学習させたいと思います。LDAトピックモデルのパラメータが沢山ありますが、今回はトピック数(n_components)のみのチューニングを行います。ダウンロードしたニュースデータから見れば、IT、家電、エンターテイメント等少なくともIT、恋愛、スポーツ、生活/娯楽の4種類が存在しますので、最適のトピック数(n_components)を特定するために、GridSearchに4,5,6,7,8,9の中から最適値を選んでもらいます。GridSearchは非常に便利なチューニング方法で、scikit-learnのGridSearchCVを使います。
from sklearn.model_selection import GridSearchCV
from sklearn.decomposition import LatentDirichletAllocation as LDA
def gridsearch_best_model(tfidf_data, plot_enabled=True):
# Define Search Param
n_topics = [4,5,6,7,8,9]
search_params = {'n_components': n_topics}
# Init the Model
lda = LDA(max_iter=25, # Max learning iterations
learning_method='batch',
random_state=0, # Random state
n_jobs = -1, # Use all available CPUs)
)
# Init Grid Search Class
model = GridSearchCV(lda, param_grid=search_params)
# Do the Grid Search
model.fit(tfidf_data)
# Best Model
best_lda_model = model.best_estimator_
# Model Parameters
print("Best Model's Params: ", model.best_params_)
# Log Likelihood Score
print("Best Log Likelihood Score: ", model.best_score_)
# Perplexity
print("Model Perplexity: ", best_lda_model.perplexity(tfidf_data))
# Get Log Likelyhoods from Grid Search Output
log_likelyhoods_score = [round(score) for score in model.cv_results_["mean_test_score"]]
if plot_enabled:
# Show graph
plt.figure(figsize=(12, 8))
plt.plot(n_topics, log_likelyhoods_score)
plt.title("Choosing Optimal LDA Model")
plt.xlabel("Numer of Topics")
plt.ylabel("Log Likelyhood Scores")
plt.show()
return best_lda_model
best_lda_model = gridsearch_best_model(tfidf_data)

トピック数を4,5,6,7,8,9それぞれに設定した際に、LDAトピックモデルのLog-Likelihood値を計算して、グラフ化しました。基本的にはLog-Likelihoodの数値が高いほどモデルが良いと考えられます。従って、グラフから最適なトピック数(n_components)が5になるのが分かります。
注意して欲しいのは、私たちは先に主観的に「ダウンロードしたニュースデータから見れば、IT、家電、エンターテイメント等少なくともIT、恋愛、スポーツ、生活/娯楽の4種類が存在します」という仮説を前提にしています。特に教師なし学習において仮説が非常に重要です。
今回の場合、仮説を立てずに、トピック数を1からGridSearchすると、最適値が1になってしまい、つまり全ての記事が同じトピックを話ているということになってしまいます。
また、トピックモデルを評価する時にperplexityとcoherenceもよく使われる指標です。perplexityは低ければ低いほど、coherenceは高ければ高いほどモデルが良いとされています。perplexityはテキストのコンテキストと単語の繋がりを考慮しないため、perplexityとcoherenceの両方を使う必要があります。
各トピックを可視化して理解しよう
各トピック内の語彙をWordCloudで可視化できますが、今回はWordCloudの可視化だけだと各トピックの解釈がやり辛いので、合わせてトピック内出現頻度上位200語彙も出して確認します。
def print_topics(model, count_vectorizer, n_top_words):
fig = plt.figure(figsize=(15,8))
words = count_vectorizer.get_feature_names()
for topic_idx, topic in enumerate(model.components_):
print("\nTopic #", topic_idx, ":")
long_string = ','.join([words[i] for i in topic.argsort()[:-n_top_words - 1:-1]])
print(long_string)
topic_wordcloud(topic_idx, fig, long_string)
# show plots
fig.tight_layout()
fig.show()
def topic_wordcloud(topic_idx, fig, long_string):
ax = fig.add_subplot(2, 3, topic_idx + 1)
wordcloud = WordCloud(font_path=font_path, background_color="white", max_words=1000, contour_width=3, contour_color='steelblue')
wordcloud.generate(long_string)
ax.imshow(wordcloud)
ax.set_title('Topic '+str(topic_idx))
number_words = 500
print_topics(best_lda_model, count_vectorizer, number_words)
Topic # 0 : 少林寺,釣れる,国生,身幅,袖丈,てんかん,紫陽花,小堀,矢口,堂上,着丈,せりふ,坪井 ,馬券,草なぎ,北陸電力,ディスカバリー,国屋,若井,正一,増上寺,真里,ウラン,大沢,配当 ,あつみ温泉,死産,樹生,慈悲,わいせつ,スペースシャトル,千石,江利,祇園,三門,せってい,高松 ,別館,湯治,墨客,芭蕉,詩歌,弘法大師,上乗せ,東京タワー,芝大門,アルカリ性,真四角,あじさい ,疎か,えらべる,ボディガード,加茂,シングルスカル,全速力,ダブルスカル,文人,北松戸,早稲田大 ,志賀,馬連,大雄,悪逆,悟道,南無阿弥陀仏,安城,宇多津,黒部,富士川,今治,西大寺 ,小麦色,包み込める,総一,彰彦,ハッブル,ミール,万感,少林寺拳法,お悔やみ,一日中,詐称 ,上塗り,金沢,裏鬼門,ジオラマ,崇める,地蔵,総門,勝運,霊廟,上野寛永寺,阿弥陀如来,安国 ,菩提寺,胎児,厄除け,越谷,ゆめタウン,重罰,食い違う,赤信号,凶器,鈴鹿,北口,四日市,三郷 ,致死,まつる,見おろす,二の宮,豊橋,城東,河辺,中山寺,江南,久居,釣り針,商業高校前 ,北長池,花堂,魚津,上里,袋町,近畿日本鉄道,上之町,元町,河原町,東予,上中居,神の倉 ,和戸,袋井,せんげ,南貴崎,みたけ,宝来,笠松,氷見,安中,花台,段原,色内,砥部,西梅田 ,高平,常滑,南大阪線,新清水,田家,宇佐,新居浜,高浜,箱崎宮前,水堂,幸田,札幌南 ,等々力,小菅,藤野,湯本,潮江,八乙女,御経塚,東武動物公園,名駅南,東金沢,郡上,道頓堀 ,小山東,五橋,さば,不動寺,渋川,志度,銀天,扇田,麻布十番,国府,南町,三ケ,福井東,嬉野 ,津原,津幡,大博,四番丁,東向島,大坪,苦竹,土橋,二川,室蘭,北大通,神郡,牟佐,大府 ,北光,東川口,伊万里 Topic # 1 : 長友,河本,川島,川澄,真司,宇佐美,俊輔,槙野,ドルトムント,熊谷,浅尾,矢部,清武,準一,金本 ,カメルーン,マンチェスター,ウズベキスタン,奈穂美,北澤,坂田,浦和,松木,潮田,犬山,シリア,澤登,今野 ,べっち,中西,篤人,親方,みの,スカイライン,釜本,征服,徳光,アイスランド,宮間,越後,ディフェンス ,ビーチバレー,浩之,脇谷,鮫島,ケルン,哲生,草野,パラグアイ,智章,貴史,軍鶏,シチリア,東原,名波 ,深山,タジキスタン,八戸,早川,籠谷,浦田,鳥谷,レフェリー,権田,武弘,レナウン,壮行,捕鯨,召集,小宮 ,山岸,アーチェリー,大嶽,有香,しじま,亀田製菓,嗣ぐ,賀陽,官報,書記,正朗,平壌,常盤木 ,ねぶた,江原,泰幸,光喜,安太郎,文楽,駅弁,牧瀬,かくれる,大嶋,マーリー,新駅,華原 ,少林寺拳法,サイドバック,リール,浦和レッズ,楢崎,小橋,訪朝,ディフェンダー,欠番,本籍,完勝,王蟲 ,宮市,栗田,ヨーコ,大相撲,健史,康彦,文集,反旗,邦茂,金平,慶子,サンフレッチェ広島,キリンカップ ,川中,ホーク,アルベルト,貴乃花,花田,アリラン,藤枝,バイエルン,ロスタイム,駒野,法政大,心神,佳織,チームメート ,朋美,スーパーボウル,先取,善戦,タジク,烏山,mvp,紙子,セレッソ,千歳,ゼスチャー,クジラ,ホイッスル,松友 ,守弘,信太郎,偽装,ビルト,横断幕,jリーグ,難色,フェイント,踏みつける,撃墜,道民,城南,南里 ,ミュンヘン,亜希,柱谷,国交,蟹江,コートジボワール,ジンバブエ,神木,菅山,仕送り,ぎんなん,人工芝 ,法大,龍二,ジョンイル,宏樹,元木,新山,グレート,小鹿,勘太郎,祝勝,紘子,映り,協栄ジム ,トランペット,あぐねる,森島,扇原,湯郷,誤記,半旗,大神,穢れる,断絶,太極,阿波,真聖 Topic # 2 : 浅田,栄子,真央,栄華,鑑定,松村,姐さん,山葵,仙人,子宮,銭湯,がわら,勅使河原,ムカデ ,養育,わさび,良子,激辛,不妊,あやしい,梅村,多香子,披露宴,サトコ,大徳,外貨,圭子,サトミ ,妖怪,柔術,だい,慰謝,新婦,新郎,末永,モロゾフ,昌子,預金,アキ,ラサール石井,つわり,結納 ,自由形,出産,イズミ,既婚,カオル,清春,なめこ,霊感,みそ汁,父様,学童,杉崎,迷信,振袖 ,こびる,磯山,ユカ,ぶんか社,小次郎,柴犬,紀伊國屋書店,水森,成城,永田,渓流,逝く ,バイキング,負け犬,母乳,アヤコ,江奈,タエ子,さやか,アラン,ぐれる,志野,卵子,鴨田,大安,肛門 ,万事休す,ヨリ,志穂,ピル,厄年,月経,美加,庭木,麻木,砲丸,朝焼け,人見知り,保育,綾部 ,かずき,山陰,ナオ,味噌,マザコン,性交,戸梶,卵巣,耳かき,前原,上祐,チューリング,範子,手相 ,内縁,検診,禁煙,生理,江戸前,坂上,和子,首ったけ,羊水,千早,京太郎,ダウン症,排卵,家柄 ,先祖,フルネーム,相川,あき竹城,気泡,同志社大,受精卵,嫁入り,チエ子,中久喜,菜子,東尾,平林 ,受診,相続,四元,マコト,ひっかかる,立石,清野,アオイ,大桃,遠吠え,ほこり,マーチン,修業,アダムス ,米ドル,照屋,萩原,焼きもち,めい,火山,精子,花子,初場所,ふじい,大悟,きょう,手鏡 ,技量,クリトリス,取組,昌枝,ワクチン,雪子,留袖,出血,浦野,恵理,正輝,江戸ッ子,奥沢,都電,墓地 ,やり投げ,テッド,産後,不貞,トス,エレメント,八卦,柘榴,三助,嫁ぐ,補欠,受精,美枝,大厄,度胸 ,河瀬,内祝い,しゃも,前厄,ベビーシッター,ユキヒロ Topic # 3 : 配合,まつ毛,ディレクトリ,転載,古閑,ヨーグルト,毛穴,成分,チーク,伊集院,クリーム,引用,恩田,カルピス ,果汁,加護,薬用,桐島,清志郎,生クリーム,イチゴ,洗顔,かたつむり,抽出,まつげ,クランベリー,三越 ,弾力,北条,ひな祭り,小夜,大島椿,ウーロン茶,漢方,フルーツ,石けん,細胞,依る,エキス,園山,キャビア ,松屋,まろやか,引き渡し,エチュード,グレープ,忌野,白桃,マスカラ,寝袋,ドーピング,ファーストレディー ,ヨモギ,モチ,井端,レモン,香料,植松,栄養素,うるおう,トリュフ,三盆,アミノ酸,湿る,ブドウ,ビスケット ,カタツムリ,反町,黄土,ラズベリー,チョコレート,亜麻,ショートケーキ,ハチミツ,バーター,薬丸,ナッシュ,たべる,木嶋 ,ローション,カネボウ,適量,みずみずしい,牧草,添加,渡し,土鍋,乗鞍,高麗,ストロベリー,石鹸,きのこ ,ほっぺ,果肉,発酵,エイボン,どろん,諸行無常,山と渓谷社,疋田,東京書籍,器用貧乏,ほうじ ,多芸,美禅,独得,献灯,砂糖,物怖じ,果実,友野,オビ,寝息,ビター,蒸し,七福神,スポンジ,放尿 ,アロエ,マロン,鉱物,梅酒,酸味,すこやか,イワナ,泡立てる,熊田,乳酸菌,フルーティー,粘液,詰め合わせ ,光井,嚢胞,宮古島,味わい,試験管,ハリ,濃縮,胃酸,お通し,エルメ,甘酸っぱい,ソープ,生姜 ,蜂蜜,とちる,升野,洗い流す,ひな人形,井下,ぶどう,いいわけ,ピーチ,人参,プルーフ,五味 ,色素,ポートワイン,たんぱく質,脂肪酸,大神宮,ソフトクリーム,下町,おとめ,ピエール,グレープフルーツ ,ブルーベリー,ライチ,インスリン,ナニワ,ファンケル,ペンシル,快刀,コニャック,マンゴー,脂質,善玉,曜子,防腐,クレンザー ,オメガ,黒ずみ,すっぽん,くすむ,黒豆,バター,村主,パウダー,ベタつく,コレステロール,根津,木崎,ムース ,かき混ぜる,馬鹿力,補修,ウィスキー,本舗,肋骨,カモミール Topic # 4 : 映画,日本,話題,女性,記事,発売,見る,機能,スマート,対応,フォン,関連,発表,情報,多い,ネット ,写真,公開,使う,モデル,利用,結婚,搭載,ドコモ,画面,行う,世界,女子,韓国,サービス,男性 ,監督,更新,紹介,仕事,作品,可能,いい,サイト,番組,人気,登場,ユーザー,撮影,表示,チェック,持つ ,製品,放送,カメラ,選手,テレビ,やる,ゲーム,知る,向け,動画,開始,好き,価格,ソフトウェア,東京 ,代表,相手,映像,予定,設定,考える,高い,シリーズ,ニュース,端末,ファン,ビデオ,語る,出る,コメント ,応募,必要,販売,感じる,提供,電話,開発,恋愛,掲示板,メール,聞く,キャンペーン,最大,行く ,イベント,機種,試合,現在,通信,購入,注目,公式,問題,開催,会社,使用,わかる,しれる,本体 ,良い,リンク,転職,商品,サイズ,結果,新しい,今年,内容,モバイル,プレゼント,ドラマ,選ぶ,作る ,パソコン,充電,バッテリー,時代,タブレット,無料,最新,入る,簡単,出演,サッカー,読む,気持ち,操作 ,編集,受ける,演じる,強い,生活,デザイン,採用,容量,楽しむ,デジタル,ページ,携帯,特集 ,ディスプレイ,アップ,接続,友達,ダウンロード,カード,データ,自身,視聴,当選,すごい,売れ筋,野球 ,クリスマス,インチ,チーム,理由,電子,話す,便利,ソフトバンク,連続,子供,使える,発言,入れる,シーン,アメリカ ,全国,執筆,変わる,言葉,食べる,料理,決定,画像,限定,ファイル,状態,分かる,違う,大きい ,始める,配信,増える,海外,一番,部分,最近,意見,セット,家族,確認

- Topic #0 地名ばかりなのでよく分からない
- Topic #1 スポーツ系ぼい
- Topic #2 結婚、出産系
- Topic #3 美容系
- Topic #4 IT通信系
新しいテキストがどのトピックを話ているかを識別してみよう
識別したテキストは以下とします:
- このクリームの成分
- このパソコンの性能が良い
美容系とIT系のテキストに構築済みのLDAトピックモデルを使ってトピックをつけてみます。
LDAトピックモデルの学習と同様に、テキストの前処理、td-idfのマトリクスの作成をやらなければいけません。
test_data_ss = pd.Series(['このクリームの成分', 'このパソコンの性能が良い'])
processed_test_data_ss = preprocess_jp(test_data_ss)
test_count_data = count_vectorizer.transform(processed_test_data_ss)
test_tfidf_data = tfidf_vectorizer.transform(test_count_data)
doc_topic_mat = best_lda_model.transform(test_tfidf_data)
dominant_topic = np.argmax(doc_topic_mat, axis=1)
test_data_df = pd.DataFrame(test_data_ss, columns=['text'])
test_data_df['topic_id'] = dominant_topic
display(test_data_df)

- 「このクリームの成分」はTopic #3で、つまり美容系のトピックです。
- 「このパソコンの性能が良い」はTopic #4で、つまりIT通信系のトピックです。
まとめ
今回の記事では、以下について説明しました:
- LDAトピックモデルのデータ準備
- LDAトピックモデル前処理の結果を可視化する
- LDAトピックモデルを構築する
- 各トピックを可視化して理解しよう
- 新しいテキストがどのトピックを話ているかを識別してみよう