機械学習_サポートベクターマシーン_pythonで実装

概要

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

本日は、先日ご紹介した「サポートベクターマシーン(support vector machine,SVM)」を実際にPython(jupyter)でやってみたのでご紹介します。 今回のゴールは「ニュース本文から、そのニュースのトピックを分類する」モデルを作成することです。 今回は「野球」と「宇宙」のいずれかに分類するモデルを作るものとします。

目次

最初に

今回のエントリでは、「ニュース本文を入れたら、そのニュースが野球に関するニュースか宇宙に関するニュースかを分類する」モデルを作成したいと思います。 先に謝っておきたいのですが、このモデルをSVMで実装するにあたって「まだご紹介できていないアルゴリズム」を使ってしまっております。
また、本来文書分類をするのなら「単語の繋がり」を考慮する必要があるのですが、今回のSVMでは考慮できておりません。 そもそも、文書分類にSVMを採用するのか、という問題になってしまうのですが、今回は諸事情により文書分類でのご紹介とさせてください。 (ごめんなさい...)

また、SVMにも色々と種類がありますが、今回は「C-SVM」と呼ばれるアルゴリズムで実装します。

手順概要

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

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

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

今回は「ニュース記事を野球の記事なのか、宇宙に関する記事なのかで分類する」という「2クラス分類」を実施するモデルを作成するため、データを「野球、もしくは宇宙のクラスのものだけ」に絞り込みます。

2.前処理

sklearnで用意されているデータなので、とても綺麗で使いやすいデータなのですが今回やりたいことを達成するために若干の前処理をしています。 前処理は本来とっても面倒臭いのですが、今回は「データセットがとても綺麗」、「SVMを実装することの全体感を把握することがメイン」といった理由から、最低限の前処理のみとなっております。具体的に実施した前処理は下記の通りです。

・文字を全て小文字にする
大文字、小文字のいずれでも同じ文字なら同じ意味の単語であろう

・クラス分類に意味がなさそうな単語を排除する
英語の「the」や「a」などは様々な文書に入り込んでくる上に、クラス分類に役立たないのであらかじめ排除する。
ストップワードを排除、という処理ですが、ストップワードについてはこちらをご覧ください。

・tf-idf計算
計算できる状態にするために変換。 「tf-idfってなんだっけ」というかたは、こちらをご参照ください。
wiki
4日目のエントリ

「次元削減」
tf-idf計算では、「形態素」の数だけ特徴量が存在するので、「疎行列だし処理効率が悪い」、「次元の呪いに引っかかる」等の問題が発生します。
そこで、「持っている情報をできるだけ欠損させず、計算する量を減らす」ために「次元削減」を実施します。

今回実施する前処理については以上です。

3.モデル作成と精度確認

前処理が終わったら早速モデルを作成していきます。具体的には「パラメータチューニング」をしていきます。 今回「C-SVM」を実装するにあたって考慮すべき点は「どれだけ誤分類を許容するか」、「汎化性能(識別超平面の係数パラメータ)」のトレードオフをどのように調整するか、です。
実際に指定するのは前者のみで、後者については「C-SVM」の計算式の中で処理されます。
このハイパーパラメータをどのように設定するかですが、はっきりいってどのくらいの値から調整していけば全くわからないので、ばっくり大きなレンジをとって精度を確認(交差検証を使います)し、少しづつ調整すべき値のレンジを絞っていきます。

4.新しいデータを入れて遊んでみる

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

今回は「野球」と「宇宙」のいずれかに分類するモデルを作成するので、下記の内容のニュースをそれぞれ2本ずつモデルに投入して結果を確かめました。
・大谷選手がエンゼルスに入団
・130億年以上前に誕生したブラックホールが発見された

コードと解説

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

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

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

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

'''
sklearnに用意されている「ニュース」のデータセットを利用します。
このデータセットから「野球」と「宇宙」に関するデータセットのみに絞り込んでいます。
'''

# データの絞り込み(野球と宇宙の文書だけ)
categories = ['rec.sport.baseball', 'sci.space'] # 今回分析対象とするカテゴリーを絞り込む
twenty_train = fetch_20newsgroups(subset='train',categories=categories, shuffle=True, random_state=42) # 上記で絞り込んだカテゴリーのデータのみを変数に入れる

print(twenty_train.target_names) # カテゴリの確認(ちゃんと絞り込めているか。)
print (len(twenty_train.data)) # 絞り込んだ結果、ニュースデータが何本になったか(1,190本のニュースデータ)
print("\n".join(twenty_train.data[0].split("\n"))) #データの内容を確認(1個の文書の中身を確認)
print(twenty_train.target_names[twenty_train.target[0]]) #データのカテゴリーを確認

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

続いて、前処理に移ります。 大枠だけ述べると、「tf-idfを計算」したのち、「次元削減」をしています。

'''2.前処理
tf-idfを計算した後に「lsa」で次元削減をしています。
次元削減をする必要性については、「次元の呪いの回避」、「処理時間の高速化」等様々な理由がございます。
「lsa」そのものについてはまだご説明できていないのですが、後日説明予定です。
'''

# 2-1.tf-idf計算
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS

tfidfv = TfidfVectorizer(lowercase=True, stop_words=ENGLISH_STOP_WORDS) # stop words処理

tfv_vector_fit = tfidfv.fit(twenty_train.data)
tfv_vector = tfv_vector_fit.transform(twenty_train.data)
print(tfv_vector.shape) # tf-idfを計算した結果、1190個の文書それぞれに21323個の特徴量が付与されました。


# 2-2.次元削減(「lsa」を使って次元削減を行います)
from sklearn.decomposition import TruncatedSVD

# 2-2-1.パラメータの調整
list_n_comp = [5,10,50,100,500,1000,5000] # 特徴量を何個に削減するか、というパラメータです。できるだけ情報量を欠損しないで、かつ次元数は少なくしたいですね。
for i in list_n_comp:
    lsa = TruncatedSVD(n_components=i,n_iter=5, random_state = 0)
    lsa.fit(tfv_vector) 
    tfv_vector_lsa = lsa.transform(tfv_vector)
    print('次元削減後の特徴量が{0}の時の説明できる分散の割合合計は{1}です'.format(i,round((sum(lsa.explained_variance_ratio_)),2)))

# 2-2-2.次元削減した状態のデータを作成
# 上記で確認した「n_components」に指定した上で、次元削減(特徴抽出)を行う
lsa = TruncatedSVD(n_components=1000,n_iter=5, random_state = 0) # 今回は次元数を1000に指定
lsa.fit(tfv_vector) 
tfv_vector_lsa = lsa.transform(tfv_vector)

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

ここでは「次元削減」をするにあたって「何次元に削減するのか」というパラメータを調整しています。 どのくらいの値に設定すればいいのかわからなかったため、ある程度大きめにレンジをとって、「どれだけ説明力を維持しているか」、を確認しています。
理想は、「次元削減する前と同じだけの説明力を維持」することなのですが、次元削減するとなるとどうしても説明力が落ちてしまうので、それなら「できるだけ説明力は維持できるようにしておきたい」といったイメージですね。

「次元削減後の特徴量が5の時の説明できる分散の割合合計は」とありますが、これが1だと「次元削減前と同じ説明力」といったイメージです。なのでできるだけこの値を1に近づけた状態で、次元を削減することを考えてパラメータを検討します。今回は「1000次元」にデータを削減することとしました。

ここまでで、やっとデータの準備ができました。
続いて、「C-SVM」モデルを作成していこうと思います。
「C-SVM」モデル作成にあたって指定しなくてはいけないパラメータは「どれだけ誤分類を許容するか」でした。 下記のコード中では「C」という変数で登場しており、この値が大きいと誤分類を許さない傾向が強くなり、識別超平面の係数ベクトルの値が大きく(汎化性能が低下) なります。
なので、このパラメータは慎重に選びたいところですが、どのくらいの値に設定すればいいのか皆目見当がつきません。なので、大きめの範囲をとりいくつかの値で実際にC-SVMを実行し制度を確認、その精度を元にパラメータを指定する方針をとります。
まずは、Cを「10-4, 10-3, 10-2, 10-1, 100, 101, 102, 103, 104, 105」でC-SVMを実行し、精度を確認(データを5個に分割した上で交差検証)します。

'''3.モデル作成と精度確認
今回実装する「C-SVM」では「識別超平面の係数(汎化性能につながる)」と「誤分類をどれだけ許容するか」の2つの要素が調整すべきパラメータです。
しかし、実際に人間が指定して調整する必要があるのは「誤分類をどれだけ許容するか」だけです。
(コード中では「C」と表記しています)

なので、この「誤分類をどれだけ許容するか」を「GridSearch」を使って調整します。
調整といってもどのくらいの値にすればいいのかもよくわからないので、最初は大きなレンジでざっくり精度を確認して見ていき、徐々にレンジを絞っていきます。
'''


# 3-1.ハイパーパラメータの調整(1回目)

from sklearn.grid_search import GridSearchCV # 3-1-1.モジュールのインポート
from sklearn import svm # 3-1-2.モジュールのインポート

'''パラメータ調整(1回目)
Cが大きいほど「誤分類を許さない」
Cが大きくなると、「係数パラメータの値が大きくなる」≒「汎化性能が低下する可能性が高まる」
パラメータがどのくらいだといいのかの勘所がないので、最初は大きめのレンジで確認します。
'''
tuned_parameters = [{'C':  [1e-4,1e-3, 1e-2, 1e-1, 1, 10, 100, 1000,10000,100000]}]# 3-1-3.確認するパラメータを指定

# 上記で用意したパラメーターごとに交差検証を実施。次のパラメータの調整のレンジを確認する。
clf = GridSearchCV(svm.SVC(), tuned_parameters, scoring="accuracy",cv=5, n_jobs=-1)
clf.fit(tfv_vector_lsa, twenty_train.target) # 3-1-4.学習
for params, mean_score, all_scores in clf.grid_scores_:
        print ("{0},精度:{1} ,標準誤差=(+/- {2}) ".format(params, round((mean_score),3), round((all_scores.std() / 2),3))) # 各パラメータごとの精度を確認

上記コードの実行結果がこちらです。

ちょっと見づらいのですが、3つの要素を表示しています。
一番左のCが今回指定した「誤分類を許さない」かの値です。
続いて、真ん中がそのCの値の時に交差検証をした結果の精度です。こちらがある程度高いCを指定する必要があります。(今回の場合だと、C=10以下は微妙そうですね...。)
一番右の値は標準誤差ですが、この値についてはどのCの時もそれほど問題があるようには見えません。

この時点ではまだCの値を決められません。もう少しCの範囲を絞った上でもう一回同じ処理を実行し、Cの調整すべき範囲を絞っていきます。

'''
上記の結果、Cが100より大きいといい感じですね...
また、1000以上にしても変化はなさそうなので、この辺りでもう一回パラメータを調整してみます。
'''

# 3-2.ハイパーパラメータの調整(2回目)
tuned_parameters = [{'C':  [ 30, 50, 100, 300, 500, 700, 1000]}] # 3-2-1.パラメータを確認する範囲を絞った

clf = GridSearchCV(svm.SVC(), tuned_parameters, scoring="accuracy",cv=5, n_jobs=-1)
clf.fit(tfv_vector_lsa, twenty_train.target) # 3-2-2.学習
for params, mean_score, all_scores in clf.grid_scores_:
        print ("{0},精度:{1} ,標準誤差=(+/- {2}) ".format(params, round((mean_score),3), round((all_scores.std() / 2),3))) # 3-2-3.各パラメータごとの精度を確認

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

Cを30からとって確認してみました。この結果から、今回はC=300でモデルを作成して先に進もうと思います。

# 3-3.SVMモデル作成
clf = svm.SVC(C=300) # 3-3-1.Cを指定
clf.fit(tfv_vector_lsa, twenty_train.target) # 3-3-2.学習

さて、ここまででC-SVMのモデル作成は一旦完了とします。この後、実際にニュース文を入れてみて「野球の記事なのか、宇宙の記事なのか」について分類できるか確認してみようと思います。
早速大谷選手やブラックホールについてのニュース記事をモデルに入れて確かめたいのですが、先に簡単な短い文書で確認してみます。

'''4.新しいデータを入れて遊んでみる
'''

# 4-1.基礎確認(とても短い文書を入れてみて、簡単にモデルの確認をする)

test = ['i love baseball','I go to the baseball field' ,'i want to be an astronaut', 'Work at NASA'] # 4-1-1.適当にデータを作成

# 前処理
test_tfv = tfv_vector_fit.transform(test) # 4-1-2.tf-idf計算
test_lsa = lsa.transform(test_tfv) # 4-1-3.lsaで次元削減

# 予測
test_predict = clf.predict(test_lsa)
print(test_predict) # 4-1-4.予測結果

野球系の文書2つ、宇宙に関する文書を2つテスト用データとして作成し、モデルに入れた結果どのように分類するかを確認してみました。結果は下記の通りですが、どうやら野球が「0」、宇宙が「1」のクラスに分類されているようです。

では続いて、実際にyahooニュースから「大谷選手のエンゼルス入団」、「130億年以上前に誕生したブラックホールの発見」についてのニュースをgoogle翻訳で英語にした文書をこのモデルに入れてみようと思います。

'''
yahooニュースから大谷選手がエンゼルス入団を決定した記事を2つチョイスして、google翻訳で英語にしたものを利用しています。
大谷選手のニュースは「0」というクラスに、ブラックホールについてのニュースは「1」というクラスに分類されています。
先ほど確認した結果と比較すると問題なく分類されていそうです。
'''

# 5-2.yahooニュースとgoogle翻訳を使った確認
# 5-2-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".', 
'The team, including the Carnegie Institute of America, announced that he found a super huge black hole that was born more than 13 billion years ago.According to the team, it is the oldest in observation history, the mass (weight) is about 800 million times of the sun. On the 7th, the paper was published in English science journal Nature, which is the result that leads to the elucidation of the evolution of the early universe.',
'In the universe about 13.1 billion light years away from the earth, the furthest and oldest black hole found so far was observed. It existed at the time when the universe was born, and existence at this time surprises scientists. The research was published in science journal Nature.'
] 

# 5-2-2.前処理
test_tfv = tfv_vector_fit.transform(test) # tf-idf計算
test_lsa = lsa.transform(test_tfv) # lsaで次元削減

# 5-2-3.予測
test_predict = clf.predict(test_lsa)
print(test_predict) # [0 0 1 1]

どうやらちゃんと分類されていそうですね。   

まとめ

今回のエントリーは以上です。
今回は2クラス分類をご紹介しましたが、他クラス分類への応用も容易に対応可能です。 SVMでは、線形分離可能なものしか分類できない(なのでスラック変数等を使って誤分類を許容しつつモデル作成をするのですが)という弱点があるのですが、非線形分離したい場合は「カーネル関数」を使って対応することがあります。

という訳で明日は、じょんすみすによるサポートベクターマシーンカーネル編についてのご紹介です。