機械学習_潜在意味解析_pythonで実装

2017.12.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

概要

こんにちは、データインテグレーション部のyoshimです。
この記事は機械学習アドベントカレンダー21日目のものとなります。

本日は、先日ご紹介した「潜在意味解析」を実際にPython(jupyter)でやってみたのでご紹介します。 今回はLDAのアルゴリズムの理解度を向上するために、「ニュースを分類するモデル」を作成してみようと思います。

目次

1.最初に

昨日のブログではLSA,pLSA,LDAについてご紹介しましたが、今回は「LDA」で実装します。

2.手順概要

今回の処理の流れは下記の通りです。

1.必要なモジュールとデータセットの準備

sklearnに用意されている「ニュースセット」のデータを利用します。
5日目のアドベントでご紹介した「tf-idf」のエントリと同じデータを利用します。)
このデータの詳細については、sklearnのチュートリアルをご覧いただきたいのですが、ざっくり説明しますと、「英語のニュース記事が1万本ほどあり、各ニュースが、「macのハード」や「オートバイ」など20カテゴリのいずれかに分類されている」データです。

LDAの理解度向上にはちょうどいいと思ったので、今回はデータを「野球、ホッケー、mac、windows」のクラスのものだけに絞り込んでいます。
この4クラスに絞り込んでいるのですが、トピックモデルで分類したら2クラスになるんじゃないかな(スポーツ、ハード)と直感的に想定できます。  

2.前処理

今回もいつも通り最低限の前処理のみとなっております。具体的に実施した前処理は下記の通りです。

・文字を全て小文字化
・stop words削除
・全文書の1割以上の文書に出現する単語の削除
・5回以上出現する単語のみ
・tf-idf計算

3.モデル生成

前処理が終わったら早速モデルを作成していきます。具体的には「パラメータチューニング」をしていきます。 今回は「トピック数」と「反復回数」を調整しました。

4.トピックの確認

上記で確認したパラメータを指定してモデルを生成できたので、各トピックにどのような単語、文書が割り当てられているかを確認しています。
(LDAは教師無し学習なので、分類結果を人間が確認して解釈する必要があります。)
結果をいうと、当初の想定通りスポーツとハードの2クラスに分類されているようでした。

5.テストデータを入れて確認

モデルに新しいデータを入れてみてどのように分類できているかを確認してみます。
具体的には、「yahooニュースの記事をgoogle翻訳で英語にしたデータ」を使って、ちゃんと分類できているか確認してみようと思います。

今回は「スポーツ」と「ハード」のいずれかに分類するモデルになっていると思われるので、下記の内容のニュースをモデルに投入して結果を確かめました。
・大谷選手がエンゼルスに入団×2
・macproの発売について
・macへの不正ログイン可能のニュース

3.コードと解説

では、実際にコードの解説をしていこうと思います。

まずは、「1.必要なモジュールとデータセットの準備」からです。
今回は、sklearnに用意されているデータセットを使います。

'''1.必要なモジュールとデータセットの準備
sklearnに用意されている「ニュース」のデータセットを利用します
'''

# モジュールのインポート
from sklearn.datasets import fetch_20newsgroups 

'''
sklearnに用意されている「ニュース」のデータセットを利用します。
'''

# カテゴリーの絞り込み
categories = ['rec.sport.baseball', 'rec.sport.hockey', 'comp.sys.mac.hardware', 'comp.windows.x']

# トレーニング用、検証用のデータセット作成
twenty_train = fetch_20newsgroups(subset='train',categories=categories, shuffle=True, random_state=42)
twenty_test = fetch_20newsgroups(subset='test',categories=categories, shuffle=True, random_state=42)

# データの中身確認
print(twenty_train.target_names) # カテゴリの確認(どんなカテゴリがあるのか)
print (len(twenty_train.data)) # ニュースデータが何本あるのか(2,368本のニュースデータ)
print (len(twenty_test.data)) # ニュースデータが何本あるのか(1,576本のニュースデータ)
print("\n".join(twenty_train.data[0].split("\n"))) #データの内容を確認(1個の文書の中身を確認)
print(twenty_train.target_names[twenty_train.target[0]]) #データのカテゴリーを確認

まずは、今回利用する「sklearn」から「ニュース」のデータを取得し、「野球とホッケー、macのハード、windowsに関するニュースのみ」にデータを絞り込んでいます。

トレーニング用データセットが2,368本、検証用データセットが1,576本用意できました。

続いて、前処理に移ります。  大枠だけ述べると、「不要な単語を削除」、「tf-idf計算」をしています。

'''2.前処理
前処理で実施したものは下記の4点。
・文字を全て小文字化
・stop words削除
・全文書の1割以上の文書に出現する単語の削除
・5回以上出現する単語のみ
'''

# tf-idf計算した時の単語数を確認してみる
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vec = TfidfVectorizer(lowercase=True, stop_words='english', max_df = 0.1, min_df = 5).fit(twenty_train.data) # 
X_train = tfidf_vec.transform(twenty_train.data) 
X_test = tfidf_vec.transform(twenty_test.data) 

print('tf-idfの計算結果の概要\n:{}'.format(repr(X_train))) # 2,368行7,562列の疎行列

# 各単語の中身を確認する
feature_names = tfidf_vec.get_feature_names()
print('\n最初の30個の単語\n:{}'.format(feature_names[:30]))
print('\n500-530\n:{}'.format(feature_names[500:530]))
print('\n1000-1030\n:{}'.format(feature_names[1000:1030]))

実行結果は下記の通りです。

前処理をした結果、2,368行7,562列の行列を得ることができました。
また、現時点でどのような単語が残っているのかを確認してみたら、よくわからない単語がポツポツ出てきました。
本来ならこういった単語も前処理の段階でどのように処理すべきか検討するべきかと思うのですが今回は省略します。

続いて、モデルを作成していこうと思います。
調整するパラメータは「トピック数」と「反復回数」の2つです。 また、モデルの評価指標として「perplexity」を利用しています。
(perplexityについてはエントロピーとパープレキシティがわかりやすかったです)

''' 3.モデル生成と評価
「perplexity」を評価指標とします。
パラメータは、「トピック数」と「反復回数」を指定します。
本来は「反復回数」は推移を確認したほうがいいとは思うのですが...
'''

# 3-1.パラメータの調整
from sklearn.decomposition import LatentDirichletAllocation
import time
n_comp = [2, 3, 4, 5, 6,10] # トピック数の指定
max_iter = [50, 100, 300, 500] # 反復回数の指定

for comp in n_comp:
    for max_i in max_iter:

        # 実行時間計測開始
        start = time.time()

        lda =LatentDirichletAllocation(n_components=comp,  max_iter=max_i, learning_method='batch', random_state=0, n_jobs=-1)
        lda_fit = lda.fit(X_train)
        lda_perp = lda.perplexity(X_test)

        # 実行時間計測終了
        elapsed_time = time.time() - start 

        # 結果の表示
        print("トピック数:{0},反復回数:{1}回,処理時間:{2}分,exp(-1. * log-likelihood per word)(perplexity):{3}".format(comp, max_i, round(elapsed_time/60, 2), round(lda_perp, 2)))

上記コードの実行結果がこちらです。
(実行時間は、処理が重かったりした時のために把握しておきたかっただけです)

とりあえず今回は、「perplexityが小さいトピック数を選択する」「perplexityが安定する反復回数を確認する」の2点を重視してパラメータを選択しようと思います。

この結果から、今回のエントリーでは下記でモデルを生成します。
・トピック数:2
・反復回数:100

''' 3.モデル生成と評価
上記の結果から、トピック数を2とします。
また、反復回数は100回くらいで良さそうですね
'''

# 3-2.先ほど求めたパラメータに応じたモデルの生成

# データの用意(トレーニング用、検証用と分けずに全セット)
twenty_train = fetch_20newsgroups(categories=categories, shuffle=True, random_state=42)
X = tfidf_vec.transform(twenty_train.data) 

# クラスの生成
from sklearn.decomposition import LatentDirichletAllocation
lda =LatentDirichletAllocation(n_components=2,  max_iter=100, learning_method='batch', random_state=0, n_jobs=-1)

# モデルの生成
lda.fit(X)
lda_X = lda.transform(X)

さて、ここまででモデルができたので、実際に各トピックにどのような単語、文書が割り当てられているかを確認してみたいと思います。

まずは各トピックにどんな単語が割り当てられているかを確認します。 「スポーツ系の単語」、「ハード系の単語」に分類されそうな気がするのですが...。

''' 4.トピックの確認
各トピックに実際にどのような単語、文書が割り当てられているのかを確認します。
'''

# 4-1.各トピックごとにどんな単語が割り当てられているのか確認

import mglearn
import numpy as np

sorting = np.argsort(lda.components_, axis=1)[:, ::-1] # 各トピックにおける重要度に応じたソート
feature_names = np.array(tfidf_vec.get_feature_names()) 

# 可視化
mglearn.tools.print_topics(topics=range(2), 
                           feature_names=feature_names,
                          sorting=sorting,
                           topics_per_chunk=2,
                          n_words=30)

なんとなくですが、トピック0にスポーツ系の単語、トピック1にハード系の単語が割り当てられていそうですね。

続いて、各トピックにどんな文書が割り当てられているのかを確認します。

''' 4.トピックの確認
各トピックに実際にどのような単語、文書が割り当てられているのかを確認します。
'''

# 4-2.各トピックごとにどんな文書が割り当てられているか

# トピック0
topic_0 = np.argsort(lda_X[:, 0])[::-1] # このトピックでもっとも重要度が高い順にソート

for i in range(0,2):
    print(twenty_train.data[topic_0[i]].split('.')[:30])
    print('\n' + '------------------------------')

トピック0の文書から2つ選んで、文書の一部だけ確認します。
内容を見る限り2つとも確かにスポーツに関する内容のようです。

本来なら「モデルの評価」についてはもっとしっかりやるべきところですが、今回はここまでとします。

続いて、新しいデータセットを投入してどのように分類されるのかを確認しようと思います。 まずは「大谷選手がエンゼルス入団が決定」した際のyahooニュースをgoogle翻訳で英語にしたものをモデルに投入しました。

'''5.テストデータを入れて確認
yahooニュースをgoogle翻訳で英語にしたものを入れてみた
'''

# 5-1.大谷選手がエンゼルス入団を決めたニュースでテスト

test = [
'Nippon Ham\'s Shohei Otani pitcher (23) who was aiming for a major transfer in the posting system decided to contract with Angels. On August 8, agent Mr. Barzero announced. In addition, Angels also announced the following statement.',
'The U.S. major league, Angels announced that the shoulder number of Shohei Otani pitcher (23) whose entry was decided will be "17".', 
]


X_test = tfidf_vec.transform(test) 
lda_test = lda.transform(X_test)
print(lda_test)

2つの記事いずれもトピック0に分類されています。トピック0は先ほど単語を確認したところ「スポーツっぽい単語」が多く割り当てられていたので「スポーツっぽいトピック」である「トピック0」に分類されているみたいです。

続いて、ハードに関するニュースもモデルに投入して確かめてみようと思います。
今回は「mac pro発売について」、「」macへの不正ログイン」のニュース2本を投入してみました。

'''5.テストデータを入れて確認
yahooニュースをgoogle翻訳で英語にしたものを入れてみた
'''

# 5-2.macpro発売、macへの不正ログインのニュース

test=[
'Apple announced on December 12 (local time) that December 14 will release a new desktop iMac Pro that will be "the most powerful Mac ever". Although the selling price in Japan is unpublished, it is 4,999 dollars (about 570,000 yen) in the United States.',
'Developer Lemi Orhan Ergin discovered a vulnerability in macOS High Sierra 10.13.1. When you click on the lock button from "User and group" in the system environment setting and enter "user name" and password in order to unlock the preference setting, enter "root" for the user name, enter the password Even without it, the lock is released.'
]


X_test = tfidf_vec.transform(test) 
lda_test = lda.transform(X_test)
print(lda_test)

mac pro発売のニュースはちょっと分類が難しかったみたいですが、両方ともトピック1に分類されています。

4.まとめ

本エントリーの実装については以上で終了です。
以下、最後に少しだけ記述しておきたいことを記述して終わります。

モデルの評価

LDAは教師無し学習なので、モデル評価時に人間が結果を解釈する必要があります。
「単純に精度がどうこうだからこのモデルは素晴らしい」といったものではないということですね。
人間が解釈する必要があるとなると、「実は面白い分析結果が出ていたのに、人間がうまく解釈できなくて見落としてしまった」、といったこともありそうな気がします。

応用例

今回はニュース記事の分類をしましたが、LDAは「口コミ」や「ID-Pos」にも応用されているみたいですね。
デロイトさんのHP
こちらの応用例を見ると、業務でLDAを使う際のモデル評価時は、「perplexity」と「トピックの潜在的意味合いを人間が解釈できるか」を行ったり来たりするのかなと思いました。

最後に

本エントリーでは簡単な実装のみだったので実力のある方にはあまり面白みのないエントリーだったかもしれませんが、これから機械学習を勉強しようという方の参考になれたら幸いです。
明日は、じょんすみすによる「バッチ学習とオンライン学習について」についてのご紹介です。

5.参考文献(順不同)

特に「Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎」については大いに参考にさせていただいた。
- Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎
- デロイトさんのHP
- エントロピーとパープレキシティ