Python for NLPを紹介する連載の6回目です。前回は、PythonのScikit-Learnライブラリを使って、Twitterデータのセンチメント分析を行う方法についてお話しました。今回は、自然言語処理におけるもう一つの重要なアプリケーションであるトピックモデリングについて勉強します。Pythonでトピックモデリングを行う方法を見ていきましょう。
トピックモデリングとは
トピックモデリングは、大量のテキストデータをグループ分けして分析する教師なし手法である。トピックモデリングでは、テキストデータにはラベルが付与されていない。むしろ、トピックモデリングは、類似した特性に基づいて文書をクラスタにグループ化しようとする。
トピックモデリングの典型的な例として、同じカテゴリに属する大量の新聞記事をクラスタリングすることが挙げられる。つまり、同じトピックを持つドキュメントをクラスタリングするのである。ここで重要なことは、正解がないため、トピックモデリングの性能を評価することは非常に難しいということである。1つのクラスタの文書間に類似の特性を見出し、それに適切なラベルやトピックを割り当てるかどうかは、ユーザー次第である。
トピックモデリングには主に2つのアプローチが用いられる。潜在的ディリクレ配分法と非負行列因子法である。次の章では、これら2つのアプローチを簡単にレビューし、Pythonでのトピックモデリングにどのように適用できるかを見ていきます。
潜在的ディリクレ配分法(LDA)
LDAは、2つの一般的な仮定に基づいている。
- 類似した単語を持つ文書は、通常、同じトピックを持っている。
- 似たような単語を持つドキュメントは同じトピックである。
これらの仮定は、同じトピックを持つ文書、例えば、ビジネスのトピックは、「経済」、「利益」、「株式市場」、「損失」などのような単語を持っているので、理にかなっています。第二の仮定は、これらの単語が複数の文書で一緒に頻繁に出現する場合、それらの文書は同じカテゴリに属する可能性があることを述べている。
数学的には、上記2つの仮定は以下のように表される。
- 文書は潜在的なトピックの確率分布である。
- トピックは単語に対する確率分布である
Pythonで作るLDAによるトピックモデリング
このセクションでは、トピックモデリングのためのLDAを実装するためにPythonをどのように使用できるかを見ていきます。データセットはKaggleからダウンロードすることができます。
このデータセットには、食品カテゴリの異なる製品に関するユーザーレビューが含まれています。LDAを使用して、ユーザーレビューを5つのカテゴリにグループ化する予定です。
最初のステップは、いつものように、必要なライブラリとともにデータセットをインポートすることです。次のスクリプトを実行してください。
import pandas as pd
import numpy as np
reviews_datasets = pd.read_csv(r'E:DatasetsReviews.csv')
reviews_datasets = reviews_datasets.head(20000)
reviews_datasets.dropna()
上記のスクリプトでは、pandasライブラリのread_csv
メソッドを使用してデータセットをインポートしています。元のデータセットには約500k件のレビューが含まれています。しかし、メモリの制約のため、最初の20kレコードに対してのみLDAを実行します。上のスクリプトでは、最初の20k行をフィルタリングし、データセットからNULL値を削除しています。
次に、データを調べるために head()
関数を用いてデータセットの最初の5行を表示します。
reviews_datasets.head()
出力では、次のようなデータが表示されます。
LDAは “Text “カラムに適用されます。これはレビューを含んでいるので、残りのカラムは無視されます。
レビュー番号350を見てみましょう。
reviews_datasets['Text'][350]
出力では、次のようなレビューテキストが表示されます。
'These chocolate covered espresso beans are wonderful! The chocolate is very dark and rich and the "bean" inside is a very delightful blend of flavors with just enough caffine to really give it a zing.'
LDAを適用する前に、データ内のすべての単語の語彙を作成する必要があります。前回の記事で、count vectorizerの助けを借りてそれを行うことができたことを思い出してください。以下のスクリプトを見てください。
from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer(max_df=0.8, min_df=2, stop_words='english')
doc_term_matrix = count_vect.fit_transform(reviews_datasets['Text'].values.astype('U'))
上のスクリプトでは、 sklearn.feature_extraction.text
モジュールの CountVectorizer
クラスを使って、文書用語の行列を作っている。ここでは、文書の80%以下に出現し、少なくとも2つの文書に出現する単語のみを含めるように指定する。また、ストップワードはトピックモデリングにあまり寄与しないので、すべて削除します。
それでは、文書項行列を見てみましょう。
doc_term_matrix
出力。
<20000x14546 sparse matrix of type '<class 'numpy.int64'=""'
with 594703 stored elements in Compressed Sparse Row format>
20k文書が14546次元のベクトルで表現されていることから、我々の語彙は14546語であることがわかる。
次に、LDAを用いてトピックを作成し、各トピックにおける語彙の各単語の確率分布を求める。以下のスクリプトを実行する。
from sklearn.decomposition import LatentDirichletAllocation
LDA = LatentDirichletAllocation(n_components=5, random_state=42)
LDA.fit(doc_term_matrix)
上記のスクリプトでは、sklearn.decomposition
ライブラリの LatentDirichletAllocation
クラスを用いて、文書-用語行列に対してLDAを行っています。パラメータ n_components
には、テキストを分割するカテゴリ(トピック)の数を指定する。パラメータ random_state
(別名シード)は 42 に設定されており、私のような結果が得られるようになっています。
語彙からランダムに単語を取得してみましょう。カウントベクタライザには語彙のすべてが格納されていることが分かっています。get_feature_names()`メソッドを使って、フェッチしたい単語のIDを渡せばいいのです。
次のスクリプトは、語彙からランダムに10個の単語を取得します。
import random
for i in range(10):
random_id = random.randint(0,len(count_vect.get_feature_names()))
print(count_vect.get_feature_names()[random_id])
出力は以下のようになります。
bribe
tarragon
qualifies
prepare
hangs
noted
churning
breeds
zon
chunkier
最初のトピックを得るために、最も確率の高い10個の単語を見つけよう。最初のトピックを取得するには、components_
属性を使い、値として0インデックスを渡します。
first_topic = LDA.components_[0]
最初のトピックには、トピック1に対する14546個の単語の確率が含まれています。確率の値に従ってインデックスをソートするには、 argsort()
関数を使用します。ソートされると、確率が最も高い10個の単語は配列の最後の10個のインデックスに属するようになります。次のスクリプトは、最も高い確率を持つ10個の単語のインデックスを返します。
top_topic_words = first_topic.argsort()[-10:]
出力。
array([14106, 5892, 7088, 4290, 12596, 5771, 5187, 12888, 7498,
12921], dtype=int64)
これらのインデックスは、 count_vect
オブジェクトから単語の値を取得するために使用されます。
このようにすることができます。
出力では、次のような単語が表示されるはずです。
for i in top_topic_words:
print(count_vect.get_feature_names()[i])
この単語は、最初のトピックがお茶に関するものである可能性を示しています。
では、5つのトピックのうち、最も確率の高い10個の単語を表示してみましょう。
water
great
just
drink
sugar
good
flavor
taste
like
tea
出力はこのようになります。
for i,topic in enumerate(LDA.components_):
print(f'Top 10 words for topic #{i}:')
print([count_vect.get_feature_names()[i] for i in topic.argsort()[-10:]])
print('
')
この出力は、2番目のトピックがチョコレートなどについてのレビューを含んでいる可能性があることを示しています。同様に、3番目のトピックは、ソーダやジュースについてのレビューを含むかもしれません。すべてのカテゴリーに共通の単語がいくつかあることがわかります。これは、ほとんどすべてのトピックで使用される単語がいくつかあるためです。例えば、「good」、「great」、「like」等です。
最後のステップとして、テキストのトピックを格納するカラムを元のデータフレームに追加します。そのためには、LDA.transform()
メソッドを使い、文書-用語の行列を渡します。このメソッドは、各文書に対して全てのトピックの確率を割り当てます。以下のコードを見てください。
Top 10 words for topic #0:
['water', 'great', 'just', 'drink', 'sugar', 'good', 'flavor', 'taste', 'like', 'tea']
Top 10 words for topic #1:
['br', 'chips', 'love', 'flavor', 'chocolate', 'just', 'great', 'taste', 'good', 'like']
Top 10 words for topic #2:
['just', 'drink', 'orange', 'sugar', 'soda', 'water', 'like', 'juice', 'product', 'br']
Top 10 words for topic #3:
['gluten', 'eat', 'free', 'product', 'like', 'dogs', 'treats', 'dog', 'br', 'food']
Top 10 words for topic #4:
['cups', 'price', 'great', 'like', 'amazon', 'good', 'br', 'product', 'cup', 'coffee']
出力に (20000, 5) とありますが、これは各文書が5つの列を持ち、各列が特定のトピックの確率値に対応していることを意味します。最大値を持つトピックインデックスを見つけるには、argmax()
メソッドを呼び出し、軸パラメータに1を渡せばよい。
次のスクリプトは、データフレームにトピックの列を追加し、その列の各行にトピックの値を代入する。
topic_values = LDA.transform(doc_term_matrix)
topic_values.shape
それでは、データセットがどのように見えるか見てみましょう。
reviews_datasets['Topic'] = topic_values.argmax(axis=1)
出力。
出力にトピックの新しいカラムがあるのがわかるでしょう。
非負行列因子法(NMF)
前節では、LDAをトピックモデリングに利用する方法を見た。本節では、非負行列分解をトピックモデリングに利用する方法を説明する。
非負行列分解は教師あり学習の一つであり、クラスタリングと次元削減を行うことができる。TF-IDFと組み合わせることで、トピックモデリングを行うことができる。ここでは、Pythonを用いて非負行列分解を行い、トピックモデリングを行う方法を説明する。
PythonによるNMFによるトピックモデリング
このセクションでは、前セクションで使用したのと同じデータセットに対してトピックモデリングを実行します。手順も非常に似ていることがわかると思います。
まず、データセットをインポートします。
reviews_datasets.head()
前節ではTe count vectorizerを用いたが、NMFはTFIDFで動作するため、本節ではTFIDF vectorizerを用いる。TFIDFを用いて文書項行列を作成する。以下のスクリプトを見てください。
import pandas as pd
import numpy as np
reviews_datasets = pd.read_csv(r'E:DatasetsReviews.csv')
reviews_datasets = reviews_datasets.head(20000)
reviews_datasets.dropna()
文書用語行列が生成されたら、すべてのトピックの語彙の確率を含む確率行列を作成することができます。これには sklearn.decomposition
モジュールの NMF
クラスを使用する。以下のスクリプトを見てみよう。
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer(max_df=0.8, min_df=2, stop_words='english')
doc_term_matrix = tfidf_vect.fit_transform(reviews_datasets['Text'].values.astype('U'))
前のセクションでやったように、語彙からランダムに10個の単語を取得してみよう。
from sklearn.decomposition import NMF
nmf = NMF(n_components=5, random_state=42)
nmf.fit(doc_term_matrix )
出力には、以下のような単語が表示される。
import random
for i in range(10):
random_id = random.randint(0,len(tfidf_vect.get_feature_names()))
print(tfidf_vect.get_feature_names()[random_id])
次に、最初のトピックの単語の確率ベクトルを取得し、最も高い確率を持つ10個の単語のインデックスを取得することにする。
safest
pith
ache
formula
fussy
frontier
burps
speaker
responsibility
dive
これらのインデックスを tfidf_vect
オブジェクトに渡すと、実際の単語を取得することができます。次のスクリプトを見てください。
first_topic = nmf.components_[0]
top_topic_words = first_topic.argsort()[-10:]
出力はこのようになる。
for i in top_topic_words:
print(tfidf_vect.get_feature_names()[i])
トピック1の単語はトピック1がチョコレートのレビューを含むかもしれないことを示しています。では、それぞれのトピックについて、最も高い確率を持つ10個の単語を表示してみましょう。
really
chocolate
love
flavor
just
product
taste
great
good
like
上のスクリプトの出力は次のようになる。
for i,topic in enumerate(nmf.components_):
print(f'Top 10 words for topic #{i}:')
print([tfidf_vect.get_feature_names()[i] for i in topic.argsort()[-10:]])
print('
')
トピック1の単語は、このトピックがコーヒーについてのレビューを含んでいることを示しています。同様に、トピック2の単語は、ソーダやジュースについてのレビューが含まれていることを表しています。トピック3は、またもや飲み物についてのレビューが含まれています。最後に、トピック4は、”cat”、”dog”、”treat “などの単語を含んでいるので、動物性食品に関するレビューが含まれている可能性がある。
次のスクリプトは、データセットにトピックを追加し、最初の5行を表示する。
Top 10 words for topic #0:
['really', 'chocolate', 'love', 'flavor', 'just', 'product', 'taste', 'great', 'good', 'like']
Top 10 words for topic #1:
['like', 'keurig', 'roast', 'flavor', 'blend', 'bold', 'strong', 'cups', 'cup', 'coffee']
Top 10 words for topic #2:
['com', 'amazon', 'orange', 'switch', 'water', 'drink', 'soda', 'sugar', 'juice', 'br']
Top 10 words for topic #3:
['bags', 'flavor', 'drink', 'iced', 'earl', 'loose', 'grey', 'teas', 'green', 'tea']
Top 10 words for topic #4:
['old', 'love', 'cat', 'eat', 'treat', 'loves', 'dogs', 'food', 'treats', 'dog']
上記のコードの出力は以下のようになる。
見ての通り、各レビューにトピックが割り当てられ、それはNMF法を用いて生成されたものです。
結論
トピックモデリングは自然言語処理において最も注目されている研究分野の一つである。ラベル付けされていない大量のテキストデータをグループ化するために使用される。この記事では、トピックモデリングの2つのアプローチについて説明しました。今回は、Pythonのライブラリを使って、Latent Dirichlet AllocationとNon-Negative Matrix Factorizationをトピックモデリングに使用する方法について説明しました。
</class