Python for NLPの連載は今回で11回目、Gensimライブラリの連載は2回目です。
前回は、PythonのGensimライブラリについて簡単に紹介しました。
単語とそれに対応する数値 ID を対応付ける辞書を作成する方法を説明しました。
さらに、辞書から bag of words コーパスを作成する方法についても説明しました。
今回は、Gensimライブラリを使ってトピックモデリングを行う方法について勉強します。
PythonのScikit-Learnライブラリを使ってトピックモデリングを行う方法については、前回の記事で説明しました。
その中で、LDA(Latent Dirichlet Allocation)とNMF(Non-Negative Matrix Factorization)を使ってトピックモデリングを行う方法を説明しました。
今回は、トピックモデリングにGensimライブラリを使用します。
トピックモデリングに採用するアプローチは、LDAとLSI(Latent Semantim Indexing)です。
必須ライブラリのインストール
Wikipediaの記事から取得したテキストに対して、トピックモデリングを行う。
Wikipediaの記事をスクレイピングするために、Wikipedia APIを使用する。
Wikipedia APIライブラリをダウンロードするには、以下のコマンドを実行する。
$ pip install wikipedia
また、AnacondaディストリビューションのPythonを使用している場合は、以下のコマンドのいずれかを使用することができます。
$ conda install -c conda-forge wikipedia
$ conda install -c conda-forge/label/cf201901 wikipedia
トピックモデルを視覚化するために、pyLDAvis
ライブラリを使用する。
ライブラリのダウンロードには、以下のpipコマンドを実行します。
$ pip install pyLDAvis
また、Anacondaディストリビューションを使用する場合は、以下のコマンドを実行することができます。
$ conda install -c conda-forge pyldavis
$ conda install -c conda-forge/label/gcc7 pyldavis
$ conda install -c conda-forge/label/cf201901 pyldavis
LDAによるトピックモデリング
この節では、LDAを用いてWikipediaの記事のトピックモデリングを行う。
まず、”Global Warming”, “Artifical Intelligence”, “Eiffel Tower”, “Mona Lisa “というトピックの4つのWikipedia記事をダウンロードする。
次に、記事の前処理を行い、次にトピックモデリングのステップを行う。
最後に、LDAモデルをどのように可視化するかを見る。
ウィキペディアの記事のスクレイピング
以下のスクリプトを実行します。
import wikipedia
import nltk
nltk.download('stopwords')
en_stop = set(nltk.corpus.stopwords.words('english'))
global_warming = wikipedia.page("Global Warming")
artificial_intelligence = wikipedia.page("Artificial Intelligence")
mona_lisa = wikipedia.page("Mona Lisa")
eiffel_tower = wikipedia.page("Eiffel Tower")
corpus = [global_warming.content, artificial_intelligence.content, mona_lisa.content, eiffel_tower.content]
上記のスクリプトでは、まず wikipedia
と nltk
ライブラリをインポートする。
また、英語の nltk
ストップワードもダウンロードする。
このストップワードは後ほど使用する。
次に、wikipedia
ライブラリの page
オブジェクトにトピックを指定して、Wikipedia から記事をダウンロードした。
返されたオブジェクトには、ダウンロードしたページに関する情報が含まれている。
ウェブページの内容を取得するには、 content
属性を利用することができる。
つの記事の内容はすべて corpus
という名前のリストに格納されている。
データ前処理
LDAによるトピックモデリングを行うには、データ辞書とBag of Wordコーパスが必要である。
前回の記事(リンク先)から、辞書と bag of words コーパスを作成するためには、トークンの形のデータが必要であることがわかる。
さらに、データセットから句読点やストップワードなどを削除する必要があります。
統一性を持たせるために、すべてのトークンを小文字に変換し、レマタイズも行います。
また、5文字未満のトークンはすべて削除する。
次のスクリプトを見てください。
import re
from nltk.stem import WordNetLemmatizer
stemmer = WordNetLemmatizer()
def preprocess_text(document):
# Remove all the special characters
document = re.sub(r'W', ' ', str(document))
# remove all single characters
document = re.sub(r's+[a-zA-Z]s+', ' ', document)
# Remove single characters from the start
document = re.sub(r'^[a-zA-Z]s+', ' ', document)
# Substituting multiple spaces with single space
document = re.sub(r's+', ' ', document, flags=re.I)
# Removing prefixed 'b'
document = re.sub(r'^bs+', '', document)
# Converting to Lowercase
document = document.lower()
# Lemmatization
tokens = document.split()
tokens = [stemmer.lemmatize(word) for word in tokens]
tokens = [word for word in tokens if word not in en_stop]
tokens = [word for word in tokens if len(word) > 5]
return tokens
上のスクリプトでは、preprocess_text
という名前のメソッドを作って、テキスト文書をパラメータとして受け取っています。
このメソッドでは、様々なタスクを実行するために正規表現演算を使用します。
上の関数で何が起こっているのか、簡単に確認してみましょう。
document = re.sub(r'W', ' ', str(X[sen]))
上の行では、すべての特殊文字と数字をスペースに置き換えています。
しかし、句読点を削除すると、意味のない一文字がテキストに表示されます。
例えば、Eiffel's
というテキストで句読点を置き換えると、Eiffel
とs
という単語が現れます。
ここで、s
は意味を持たないので、スペースに置き換える必要があります。
次のスクリプトがそれを行う。
document = re.sub(r's+[a-zA-Z]s+', ' ', document)
上記のスクリプトは、テキスト内の1文字だけを削除します。
テキストの先頭の1文字を削除するには、次のコードを使用します。
document = re.sub(r'^[a-zA-Z]s+', ' ', document)
テキスト内の単一の空白を削除すると、複数の空白が現れることがある。
次のコードは、複数の空白を1つの空白に置き換えます。
document = re.sub(r's+', ' ', document, flags=re.I)
オンラインで文書をスクレイピングするとき、しばしば文書に b
という文字列が付加されますが、これはその文書がバイナリであることを意味します。
この接頭辞 b
を削除するために、以下のスクリプトが使われる。
document = re.sub(r'^bs+', '', document)
残りのメソッドは自明である。
文書は小文字に変換され、次にトークンに分割される。
トークンはlemmatizationされ、ストップワードが除去される。
最後に、5文字未満のトークンはすべて無視される。
残りのトークンは呼び出した関数に返される。
モデリングトピックス
このセクションがこの記事の本題です。
ここでは、Gensim ライブラリの組み込み関数がどのようにトピックモデリングに使用されるかを見ていきます。
しかしその前に、スクレイピングした4つのWikipedia記事に含まれる全てのトークン(単語)からなるコーパスを作成する必要があります。
以下のスクリプトを見てください。
processed_data = [];
for doc in corpus:
tokens = preprocess_text(doc)
processed_data.append(tokens)
上のスクリプトは単純明快である。
4つのWikipediaの記事を文字列の形で含むcorpus
リストを繰り返し実行する。
各繰り返しにおいて、先に作成した preprocess_text
メソッドにドキュメントを渡す。
このメソッドは、その特定のドキュメントに対応するトークンを返します。
トークンは processed_data
リストに格納される。
forループの最後には、4つの記事のすべてのトークンが
processed_data` リストに格納されます。
このリストを使って、辞書とそれに対応する bag of words コーパスを作成することができる。
以下のスクリプトはそれを行うものである。
from gensim import corpora
gensim_dictionary = corpora.Dictionary(processed_data)
gensim_corpus = [gensim_dictionary.doc2bow(token, allow_update=True) for token in processed_data]
次に、辞書と単語袋コーパスをpickleを使って保存する。
保存した辞書は後で新しいデータに対する予測に使う。
import pickle
pickle.dump(gensim_corpus, open('gensim_corpus_corpus.pkl', 'wb'))
gensim_dictionary.save('gensim_dictionary.gensim')
これでGensimでLDAモデルを作成するのに必要なものが揃いました。
LDA モデルの作成には gensim.models.ldamodel
モジュールの LdaModel
クラスを使用します。
LdaModel` コンストラクタの最初のパラメータに先ほど作成した bag of words コーパスを渡し、次にトピック数、先ほど作成した辞書、パス数 (モデルの反復回数) を指定する必要があります。
以下のスクリプトを実行してください。
import gensim
lda_model = gensim.models.ldamodel.LdaModel(gensim_corpus, num_topics=4, id2word=gensim_dictionary, passes=20)
lda_model.save('gensim_model.gensim')
はい、とても簡単です。
上のスクリプトでは、データセットからLDAモデルを作成し、保存しました。
次に、各トピックについて10個の単語を出力してみましょう。
そのためには、print_topics
メソッドを使います。
以下のスクリプトを実行してください。
topics = lda_model.print_topics(num_words=10)
for topic in topics:
print(topic)
出力はこのようになる。
(0, '0.036*"painting" + 0.018*"leonardo" + 0.009*"louvre" + 0.009*"portrait" + 0.006*"museum" + 0.006*"century" + 0.006*"french" + 0.005*"giocondo" + 0.005*"original" + 0.004*"picture"')
(1, '0.016*"intelligence" + 0.014*"machine" + 0.012*"artificial" + 0.011*"problem" + 0.010*"learning" + 0.009*"system" + 0.008*"network" + 0.007*"research" + 0.007*"knowledge" + 0.007*"computer"')
(2, '0.026*"eiffel" + 0.008*"second" + 0.006*"french" + 0.006*"structure" + 0.006*"exposition" + 0.005*"tallest" + 0.005*"engineer" + 0.004*"design" + 0.004*"france" + 0.004*"restaurant"')
(3, '0.031*"climate" + 0.026*"change" + 0.024*"warming" + 0.022*"global" + 0.014*"emission" + 0.013*"effect" + 0.012*"greenhouse" + 0.011*"temperature" + 0.007*"carbon" + 0.006*"increase"')
最初のトピックには、painting
, louvre
, portrait
, french
museum
などの単語が含まれています。
これらの単語は、フランスに関連する絵に関するトピックに属していると考えることができます。
同様に、2番目には、intelligence
, machine
, research
などの単語が含まれています。
これらの単語は、人工知能に関連するトピックに属していると考えることができます。
同様に、3番目と4番目のトピックの単語は、それぞれエッフェル塔と地球温暖化というトピックに属していることを指し示しています。
このように、LDAモデルはデータセット中の4つのトピックを見事に識別していることがわかる。
ここで重要なのは、LDAは教師なし学習アルゴリズムであり、実際の問題ではデータセット内のトピックについて事前に知ることはないということです。
単にコーパスが与えられ、LDAを用いてトピックを作成し、そのトピックの名前はあなた次第です。
では、このデータセットを使って8つのトピックを作ってみましょう。
1つのトピックにつき、5つの単語を出力することにします。
lda_model = gensim.models.ldamodel.LdaModel(gensim_corpus, num_topics=8, id2word=gensim_dictionary, passes=15)
lda_model.save('gensim_model.gensim')
topics = lda_model.print_topics(num_words=5)
for topic in topics:
print(topic)
出力はこのようになります。
(0, '0.000*"climate" + 0.000*"change" + 0.000*"eiffel" + 0.000*"warming" + 0.000*"global"')
(1, '0.018*"intelligence" + 0.016*"machine" + 0.013*"artificial" + 0.012*"problem" + 0.010*"learning"')
(2, '0.045*"painting" + 0.023*"leonardo" + 0.012*"louvre" + 0.011*"portrait" + 0.008*"museum"')
(3, '0.000*"intelligence" + 0.000*"machine" + 0.000*"problem" + 0.000*"artificial" + 0.000*"system"')
(4, '0.035*"climate" + 0.030*"change" + 0.027*"warming" + 0.026*"global" + 0.015*"emission"')
(5, '0.031*"eiffel" + 0.009*"second" + 0.007*"french" + 0.007*"structure" + 0.007*"exposition"')
(6, '0.000*"painting" + 0.000*"machine" + 0.000*"system" + 0.000*"intelligence" + 0.000*"problem"')
(7, '0.000*"climate" + 0.000*"change" + 0.000*"global" + 0.000*"machine" + 0.000*"intelligence"')
トピックをいくつ作るかはあなた次第です。
適切なトピックが見つかるまで、いろいろな数を試してみてください。
このデータセットでは、コーパスに 4 つの異なる記事の単語が含まれていることが既に分かっているので、適切なトピック数は 4 です。
以下のスクリプトを実行することで、4つのトピックに戻すことができます。
lda_model = gensim.models.ldamodel.LdaModel(gensim_corpus, num_topics=4, id2word=gensim_dictionary, passes=20)
lda_model.save('gensim_model.gensim')
topics = lda_model.print_topics(num_words=10)
for topic in topics:
print(topic)
今回はLDAパラメータの初期値がランダムに選ばれているため、異なる結果が得られるでしょう。
今回の結果は以下の通りである。
(0, '0.031*"climate" + 0.027*"change" + 0.024*"warming" + 0.023*"global" + 0.014*"emission" + 0.013*"effect" + 0.012*"greenhouse" + 0.011*"temperature" + 0.007*"carbon" + 0.006*"increase"')
(1, '0.026*"eiffel" + 0.008*"second" + 0.006*"french" + 0.006*"structure" + 0.006*"exposition" + 0.005*"tallest" + 0.005*"engineer" + 0.004*"design" + 0.004*"france" + 0.004*"restaurant"')
(2, '0.037*"painting" + 0.019*"leonardo" + 0.009*"louvre" + 0.009*"portrait" + 0.006*"museum" + 0.006*"century" + 0.006*"french" + 0.005*"giocondo" + 0.005*"original" + 0.004*"subject"')
(3, '0.016*"intelligence" + 0.014*"machine" + 0.012*"artificial" + 0.011*"problem" + 0.010*"learning" + 0.009*"system" + 0.008*"network" + 0.007*"knowledge" + 0.007*"research" + 0.007*"computer"')
1つ目のトピックの単語はほとんど地球温暖化に関するものであり、2つ目のトピックにはエッフェル塔に関する単語が含まれていることがわかる。
LDAモデルの評価
先に述べたように、教師なし学習モデルの評価は難しい。
なぜなら、モデルの出力をテストできる具体的な真実がないからである。
新しいテキスト文書があり、先ほど作成したLDAモデルを使ってそのトピックを見つけたいとすると、以下のスクリプトを使ってそれを行うことができる。
test_doc = 'Great structures are build to remember an event happened in the history.'
test_doc = preprocess_text(test_doc)
bow_test_doc = gensim_dictionary.doc2bow(test_doc)
print(lda_model.get_document_topics(bow_test_doc))
上記のスクリプトでは、文字列を作成し、その辞書表現を作成し、そしてその文字列をBag of Wordコーパスに変換している。
この bag of words 表現は get_document_topics
メソッドに渡される。
出力はこのようになる。
[(0, 0.08422605), (1, 0.7446843), (2, 0.087012805), (3, 0.08407689)]
この出力は、新しい文書がトピック1に属する確率が8.4%であることを示している(最後の出力にあるトピック1の単語を参照)。
同様に、この文書が2番目のトピックに属する確率は74.4%である。
2番目のトピックを見ると、エッフェル塔に関連する単語が含まれています。
このテスト文書にも、構造物や建物に関連する単語が含まれています。
したがって、2番目のトピックに割り当てられました。
LDAモデルを評価するもう一つの方法は、PerplexityとCoherence Scoreを使うことです。
良いLDAモデルの経験則として、パープレキシティスコアは低く、コヒーレンススコアは高いことが望ましいとされています。
Gensimライブラリには、LDAモデルのコヒーレンス性を求めるためのクラス CoherenceModel
が用意されており、これを使用することでLDAモデルのコヒーレンス性を求めることができます。
LdaModelオブジェクトには
log_perplexity` メソッドが含まれており、Bag of Word コーパスをパラメータとして受け取り、対応する perplexity を返すことができます。
print('
Perplexity:', lda_model.log_perplexity(gensim_corpus))
from gensim.models import CoherenceModel
coherence_score_lda = CoherenceModel(model=lda_model, texts=processed_data, dictionary=gensim_dictionary, coherence='c_v')
coherence_score = coherence_score_lda.get_coherence()
print('
Coherence Score:', coherence_score)
CoherenceModelクラスは LDA モデル、トークン化されたテキスト、辞書、および辞書をパラメータとして受け取る。
コヒーレンススコアを得るには、get_coherence` メソッドを利用する。
出力は以下のようなものである。
Perplexity: -7.492867099178969
Coherence Score: 0.718387005948207
LDAの可視化
データを可視化するには、冒頭でダウンロードした pyLDAvis
ライブラリを使用します。
このライブラリにはGensimのLDAモデル用のモジュールが含まれています。
まず、辞書、Bag of Wordコーパス、LDAモデルを prepare
メソッドに渡して、可視化の準備をする必要があります。
次に、以下のように pyLDAvis
ライブラリの gensim
モジュールで display
を呼び出します。
gensim_dictionary = gensim.corpora.Dictionary.load('gensim_dictionary.gensim')
gensim_corpus = pickle.load(open('gensim_corpus_corpus.pkl', 'rb'))
lda_model = gensim.models.ldamodel.LdaModel.load('gensim_model.gensim')
import pyLDAvis.gensim
lda_visualization = pyLDAvis.gensim.prepare(lda_model, gensim_corpus, gensim_dictionary, sort_topics=False)
pyLDAvis.display(lda_visualization)
出力では、以下のような可視化が表示されます。
上の画像の各円は1つのトピックに対応しています。
4つのトピックを使ったLDAモデルの出力から、1番目のトピックは地球温暖化、2番目のトピックはエッフェル塔、3番目のトピックはモナリザ、4番目のトピックは人工知能に関連することが分かる。
円同士の距離は、それぞれのトピックがどの程度違うかを示しています。
円2と円3が重なっているのがわかります。
これは、トピック2(エッフェル塔)とトピック3(モナリザ)に、「フランス語」「フランス」「博物館」「パリ」など、共通する単語が多いためです。
右側のいずれかの単語にカーソルを合わせると、その単語を含むトピックの円だけが表示されます。
例えば、「気候」という言葉にカーソルを合わせると、気候という言葉が含まれていないため、トピック2と4が消えることが確認できます。
気候」という言葉の出現回数のほとんどは最初のトピックに含まれるため、トピック1のサイズは大きくなります。
次の画像のように、ごく一部がトピック3に含まれています。
同様に、いずれかの円をマウスでクリックすると、そのトピックの最頻出語リストが、そのトピックでの出現頻度とともに右側に表示される。
例えば、「エッフェル塔」というトピックに対応する円2にカーソルを合わせると、次のような結果が表示されます。
2番目のトピックである「エッフェル塔」に対応するサークルが選択されていることがわかります。
右側のリストから、そのトピックに最も多く出現する用語を見ることができます。
eiffel “という用語が一番上にあります。
また、”eiffel “という用語は、このトピックの中で多く使われていることがわかります。
一方、”french “という用語を見ると、この用語の出現数の約半分がこのトピック内であることがよくわかります。
これは、トピック3、すなわち「モナリザ」にも「french」という用語がかなりの回数含まれているからです。
これを確認するには、トピック3の円をクリックし、”french “という用語にカーソルを合わせてみてください。
LSIによるトピックモデリング
前節では、LDAによるトピックモデリングの方法を見た。
ここでは、Latent Semantic Indexing (LSI)を用いてトピックモデリングを行う方法を説明します。
そのためには、LsiModel
クラスを使うだけです。
残りの処理は、以前 LDA で行った処理と全く同じです。
以下のスクリプトを見てください。
from gensim.models import LsiModel
lsi_model = LsiModel(gensim_corpus, num_topics=4, id2word=gensim_dictionary)
topics = lsi_model.print_topics(num_words=10)
for topic in topics:
print(topic)
出力はこのようになります。
(0, '-0.337*"intelligence" + -0.297*"machine" + -0.250*"artificial" + -0.240*"problem" + -0.208*"system" + -0.200*"learning" + -0.166*"network" + -0.161*"climate" + -0.159*"research" + -0.153*"change"')
(1, '-0.453*"climate" + -0.377*"change" + -0.344*"warming" + -0.326*"global" + -0.196*"emission" + -0.177*"greenhouse" + -0.168*"effect" + 0.162*"intelligence" + -0.158*"temperature" + 0.143*"machine"')
(2, '0.688*"painting" + 0.346*"leonardo" + 0.179*"louvre" + 0.175*"eiffel" + 0.170*"portrait" + 0.147*"french" + 0.127*"museum" + 0.117*"century" + 0.109*"original" + 0.092*"giocondo"')
(3, '-0.656*"eiffel" + 0.259*"painting" + -0.184*"second" + -0.145*"exposition" + -0.145*"structure" + 0.135*"leonardo" + -0.128*"tallest" + -0.116*"engineer" + -0.112*"french" + -0.107*"design"')
結論
トピックモデリングは自然言語処理における重要なタスクである。
Pythonでトピックモデリングを行うために、様々なアプローチやライブラリが存在します。
この記事では、PythonのGensimライブラリを用いて、LDAとLSIのアプローチでトピックモデリングを行う方法を紹介しました。
また、LDAモデルの結果を可視化する方法を紹介しました。