け日記

SIerから転職したWebアプリエンジニアが最近のITのキャッチアップに四苦八苦するブログ

scikit-learnでスパムメッセージを分類する(CountVectorizer + BernoulliNB)

scikit-learnを使ってナイーブベイズでスパムメッセージを分類してみます。

データセットのロード

今回はUCIで提供されているSMS Spam Collection Data Setを使います。 データセット全体で5572サンプル(内スパムは747)からなり、各サンプルはSMSのメッセージ本文とそれがスパムかどうかの2項目のみを持ちます。

UCI Machine Learning Repository: SMS Spam Collection Data Set SMS Spam Collection Dataset | Kaggle

CSVファイル(spam.csv)でダウンロードし、本文をX、スパムかどうか(目的変数)をyにそれぞれ抽出します。

import pandas as pd

original_df = pd.read_csv('spam.csv', encoding='latin-1')
# ゴミカラムの除去
original_df.drop(['Unnamed: 2', 'Unnamed: 3', 'Unnamed: 4'], axis=1, inplace=True)

original_df['v1'].value_counts()
# ham     4825
# spam     747

X = pd.DataFrame(original_df['v2'])
y = original_df['v1'].apply(lambda s: 1 if s == 'spam' else 0) # 目的変数(スパムなら1)

Bag-of-Words

Xにはまだ生の文章が入っているので、ここから特徴量を抽出する必要があります。

Bag-of-Wordsは学習サンプルに現れた語の出現頻度(どの語がいくつ現れたか)をベクトル化した特徴量です。 学習サンプルに現れた全ての語の種類数が、特徴量の数となるため、各サンプルは極めて疎なベクトルとなります。

Bag-of-Wordsを抽出する方法として、scikit-learnではCountVectorizerが提供されています。

学習データでfitした後、 vocabulary_ にアクセスして語の種類数をカウントすると2284となっており、例えば'buy'や'space'が出現していることがわかります(後ろの数字は辞書順のソート番号になっており、'buy'であれば414番目になります)。 このCountVectorizerを使って、transformで文章をベクトル化すると、2284の特徴量を持ったnumpy行列に変換されます。

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer

# 学習データ(557サンプル)とテストデータ(5015サンプル)の分離
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.1)

# CoutVectorizer
vectorizer = CountVectorizer()
vectorizer.fit(X_train['v2'])

print('Vocabulary size: {}'.format(len(vectorizer.vocabulary_)))
print('Vocabulary content: {}'.format(vectorizer.vocabulary_))
# Vocabulary size: 2284
# Vocabulary content: {'buy': 414, 'space': 1832, 'invaders': 1048, 'chance': 463, 'win': 2199, 'orig': 1445, 'arcade': 276, ...

# 文章を特徴ベクトル化
X_train_bow = vectorizer.transform(X_train['v2'])
X_test_bow = vectorizer.transform(X_test['v2'])

print('X_train_bow:\n{}'.format(repr(X_train_bow)))
print('X_test_bow:\n{}'.format(repr(X_test_bow)))
# X_train_bow:
# <557x2284 sparse matrix of type '<class 'numpy.int64'>'
#  with 7471 stored elements in Compressed Sparse Row format>
# X_test_bow:
# <5015x2284 sparse matrix of type '<class 'numpy.int64'>'
#  with 53413 stored elements in Compressed Sparse Row format>

ナイーブベイズによる分類

得られた特徴ベクトルからナイーブベイズで学習・テストさせてみます。

scikit-learnではいくつかナイーブベイズの実装が有りますが、今回は分類タスクに使われるBernoulliNBで学習・テストを行います。

結果としては、学習データでは精度97.8%と高い値をマークしましたが、テストデータでは92.0%となっているため、過学習の傾向が見られます。

from sklearn.naive_bayes import BernoulliNB

model = BernoulliNB()
model.fit(X_train_bow, y_train)

print('Train accuracy: {:.3f}'.format(model.score(X_train_bow, y_train)))
print('Test accuracy: {:.3f}'.format(model.score(X_test_bow, y_test)))
# Train accuracy: 0.978
# Test accuracy: 0.920

CountVectorizerのチューニング

min_df

CountVectorizerの vocabulary_ を見ていると意味のない語が多いことがわかります。 例えば、 080008394021250 といった数字や、maybundrstnd といったおそらくスペルミスなど、特定の文章にしか出てこない固有の語が含まれており、こうした語は未知のデータでも現れない可能性が高いため、分類には役に立たず、過学習を進めてしまいます。

CountVectorizerには min_df というオプションが用意されています。 min_dfは整数で設定し、学習データ全体で現れたサンプル数がmin_df未満の語については特徴ベクトルからは除外する、というものです。

ここでは3を設定しており、3サンプル以上現れた語のみが特徴量となります。 結果として、語数は2284から504まで減り、テストデータの精度も4.7%改善しました(92.0%→96.7%)。

vectorizer = CountVectorizer(min_df=3)
vectorizer.fit(X_train['v2'])

print('Vocabulary size: {}'.format(len(vectorizer.vocabulary_)))
# Vocabulary size: 504

X_train_bow = vectorizer.transform(X_train['v2'])
X_test_bow = vectorizer.transform(X_test['v2'])

model.fit(X_train_bow, y_train)

print('Train accuracy: {:.3f}'.format(model.score(X_train_bow, y_train)))
print('Test accuracy: {:.3f}'.format(model.score(X_test_bow, y_test)))
# Train accuracy: 0.982
# Test accuracy: 0.967

ストップワード

また stop_words というオプションもあり、ストップワードを指定することができます。 ストップワードは、文法的に特別な意味を持っている語など、分類にかかわらず多くの文章で出現する語(例えば"and"、"you"、"has"など)のことで、これらも分類の役に立たないため、特徴量から除去するのが一般的です。 stop_wordsには除去したい文字列リストの他、'engilish'を渡すと付属のストップワードを除去できます。

学習データに適用してみますと、語数は2284から2110まで減らせました。 get_stop_words()ストップワードの一覧が取得できます(“becomes"や"go"などが含まれています)。

ただし、ストップワードを除去した特徴量では、学習データ・テストデータともに精度は落ちてしまいました。 おそらく非スパムメッセージに現れやすい語(例えば"I"や"my")がストップワードとして除去されてしまったためと思われます。

vectorizer = CountVectorizer(stop_words='english')
vectorizer.fit(X_train['v2'])

print('Vocabulary size: {}'.format(len(vectorizer.vocabulary_)))
print('Stop words: {}'.format(vectorizer.get_stop_words()))
# Vocabulary size: 2110
# Stop words: frozenset({'becomes', 'go', 'former', 'describe', 'eight', 'himself', ...

X_train_bow = vectorizer.transform(X_train['v2'])
X_test_bow = vectorizer.transform(X_test['v2'])

model.fit(X_train_bow, y_train)

print('Train accuracy: {:.3f}'.format(model.score(X_train_bow, y_train)))
print('Test accuracy: {:.3f}'.format(model.score(X_test_bow, y_test)))
# Train accuracy: 0.955
# Test accuracy: 0.892

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎