こんにちは、神楽 朔です!
「機械学習って難しそう…」「プログラミングやったことないけど、AI作ってみたい!」
そんな風に思っている方も多いのではないでしょうか?
この記事では、データサイエンスコンペティションサイトSIGNATEの練習問題「スパムメール分類」を題材に、環境構築からAIモデルの学習、結果の提出までを約1時間で駆け抜けるための手順を、どこよりも丁寧に解説していきます。
プログラミング初心者の方でも、この記事の通りに進めれば、自分だけのスパムメール分類AIを完成させることができます。一緒にAI開発の世界に飛び込んでみましょう!
0. 準備:Colabの起動
今回の分析は、Googleが提供する無料のプログラミング環境Google Colaboratory (Colab) を使います。面倒な環境構築は一切不要で、Googleアカウントさえあれば誰でもすぐに始められます!
1. ライブラリのインポート
このステップでは、これから始まる分析の旅に必要な道具一式を揃えます。
# ===================================================================
# 1. ライブラリのインポート
# ===================================================================
# Pythonが出す細かい警告(warning)を非表示にして、実行結果を見やすくするおまじない
import warnings
warnings.simplefilter('ignore')
# 正規表現を扱うためのライブラリ。テキストから特定のパターンを見つけるのに使う
import re
# 複数のファイルを一度に扱うためのライブラリ。フォルダ内の全テキストファイルを読むのに便利
import glob
# データ分析の必須ライブラリ。Excelのような表形式でデータを扱う (DataFrame)
import pandas as pd
# 数値計算を高速に行うためのライブラリ。pandasの土台にもなっている
import numpy as np
# データをグラフ化(可視化)するためのライブラリ
import seaborn as sns
import matplotlib.pyplot as plt
# matplotlibで日本語のタイトルなどが文字化けしないようにするためのおまじない
# Colabで使うには !pip install japanize-matplotlib の実行が別途必要
import japanize_matplotlib
# Google ColabやJupyter Notebookでグラフをインライン表示させるためのおまじない
%matplotlib inline
# 単語の出現回数を効率よく数えるのに便利な辞書型の defaultdict をインポート
from collections import defaultdict
# 自然言語処理(NLP)のための総合ライブラリ
import nltk
# nltkで使うストップワード辞書をダウンロード (初回のみ必要)
nltk.download('stopwords')
# ストップワード("a", "the"など、分析に不要な頻出単語)のリストをインポート
from nltk.corpus import stopwords
# テキストデータの頻出単語を雲のように可視化するワードクラウドを作成するライブラリ
from wordcloud import WordCloud
# 機械学習ライブラリ scikit-learn から必要な機能を個別にインポート
# データを訓練用と検証用に分割するツール
from sklearn.model_selection import train_test_split
# テキストを単語の出現回数ベクトル(数値)に変換するツール
from sklearn.feature_extraction.text import CountVectorizer
# 今回使用するナイーブベイズ分類器のモデル (Gaussian, Multinomial)
from sklearn.naive_bayes import GaussianNB, MultinomialNB
# モデルの性能を評価する指標 (F1スコア) を計算するツール
from sklearn.metrics import f1_score
主要ライブラリの役割紹介
コードに出てきたライブラリが、それぞれどんな役割を持っているのか、もう少し詳しく見てみましょう。
-
pandas
とnumpy
:データ分析の主役コンビ-
pandas
: データをExcelのような表(DataFrame
という形式)で扱えるようにしてくれます。データの読み込み、加工、集計など、あらゆる場面で活躍する最重要ライブラリです。 -
numpy
: 高速な数値計算や配列操作が得意です。pandas
も内部でnumpy
を利用しており、データ分析の縁の下の力持ちです。
-
-
seaborn
とmatplotlib
:グラフ作成のアーティスト-
matplotlib
: Pythonのグラフ描画の基本となるライブラリです。 -
seaborn
:matplotlib
をより美しく、統計的なグラフを簡単に描けるようにしたライブラリです。今回は主にこちらを使います。
-
-
nltk
とwordcloud
:テキスト分析の専門家-
nltk
(Natural Language Toolkit): テキストデータから不要な単語(ストップワード)を削除するなど、自然言語処理特有の「お掃除」作業を行ってくれます。 -
wordcloud
: 文章の中でよく出てくる単語を、文字の大きさで表現する「ワードクラウド」を簡単に作成できます。データの概要を直感的に掴むのに役立ちます。
-
-
scikit-learn
(sklearn
):機械学習の万能ツールボックス- データの前処理から、モデルの学習、評価まで、機械学習の一連の流れをサポートしてくれる最強のライブラリです。今回はここから「データを分割する機能」や「テキストを数値化する機能」、「ナイーブベイズモデル」などを借りてきます。
これで、データを読み込んだり、グラフを作ったり、AIモデルを訓練したりする準備は万端です。
次のステップ「2. データの読み込み」では、いよいよ分析対象であるスパムメールのデータをプログラムに読み込んでいきます。
2. データの読み込み:分析の材料を準備する
料理の道具を揃えたら、次は食材をまな板の上に乗せる番です。データ分析では、生データをプログラムに読み込み、pandas
のDataFrame
という調理しやすい形に整える作業がこれにあたります。
今回のコンペティションでは、データが少し特殊な形式で提供されています。
-
train_master.csv
: どのメールがスパム(1)か、そうでないか(0)かという正解ラベルが書かれた設計図のようなファイル。 -
train2
フォルダ: 訓練用のメール本文が、1通ずつ大量のテキストファイルに分かれて保存されている。 -
test2
フォルダ: 予測対象となるテスト用のメール本文が、同様に分かれて保存されている。
これらのバラバラのデータを、一つの使いやすい表にまとめていきましょう。
2-1. マスタデータとファイル数の確認
まずは、正解ラベルが書かれているtrain_master.csv
を読み込みます。そして、「設計図に書かれているメールの数」と「実際にフォルダに入っているメールファイルの数」が一致しているかを確認します。この地味な確認作業が、後のエラーを防ぐための重要な一手になります。
# ===================================================================
# 2. データの読み込み
# ===================================================================
# --- マスタデータの読み込み ---
# read_csvでCSVファイルを読み込む。
# index_col=0 で、1列目をDataFrameのインデックス(行名)として指定する。
train = pd.read_csv('train_master.csv', index_col=0)
# --- データの確認 ---
# train.shape で、DataFrameの形状(行数, 列数)を表示する
print('--- 正解ラベルデータ ---')
print(f'形状: {train.shape}')
print(train.head()) # .head()で先頭5行を表示して中身を確認
# --- メール本文ファイル数の確認 ---
# glob.glob('train2/train_*.txt') で、指定したパターンのファイルパスを全てリストとして取得する
# len() でそのリストの要素数、つまりファイル数を数える
print('\n--- メール本文データ ---')
print(f"訓練メールのファイル数: {len(glob.glob('train2/train_*.txt'))}件")
print(f"テストメールのファイル数: {len(glob.glob('test2/test_*.txt'))}件")
実行結果の目安
訓練データもテストデータも、それぞれ 2586 件ずつあることが確認できればOKです。
2-2. 訓練/テストデータの作成
ファイル数が正しいことを確認できたら、いよいよメール本文を読み込んでいきます。glob
で見つけた大量のファイルパスを使い、forループで1つずつファイルを開き、中身をリストに溜め込んでいく、という作戦です。
# --- 訓練データの作成 ---
# 読み込んだメール本文を一時的に保存するための空のリストを用意
text_train = []
# np.sort()でファイル名の順番を揃えてからループを開始する
# これにより、正解ラベルとメール本文の対応がズレるのを防ぐ
for file_name in np.sort(glob.glob('train2/train_*.txt')):
# 'with open(...)' でファイルを開く。処理が終わると自動で閉じてくれるので安全
with open(file_name, encoding='utf-8', errors='ignore') as f:
# f.read()でファイルの中身をすべて文字列として読み込む
text = f.read()
# 読み込んだ本文をリストに追加
text_train.append(text)
# 最後に、本文リストを'text'という名前の新しい列としてtrain DataFrameに追加
train['text'] = text_train
# --- テストデータの作成 ---
# 訓練データと全く同じ手順で、テストデータも作成する
text_test = []
for file_name in sorted(glob.glob('test2/test_*.txt')):
with open(file_name, encoding='utf-8', errors='ignore') as f:
text = f.read()
text_test.append(text)
# テストデータは元になるDataFrameがないので、pd.DataFrame()で新規作成する
test = pd.DataFrame(data={'text':text_test}, index=[f'test_{str(x).zfill(4)}.txt' for x in range(len(text_test))])
# --- 完成したデータの確認 ---
print('\n--- 完成した訓練データ (先頭5行) ---')
print(train.head())
print('\n--- 完成したテストデータ (先頭5行) ---')
print(test.head())
これで、「どのメールがスパムか」という正解ラベルと、「メールの本文」が紐付いた、分析しやすいデータが手に入りました。まな板の上に、綺麗に食材が並んだ状態です。
3. データの可視化:データの特徴を探る旅 (EDA)
食材の準備が整ったら、次はその食材をじっくりと観察する番です。このプロセスを探索的データ分析 (Exploratory Data Analysis, EDA) と呼びます。データをグラフなどの視覚的な表現に変換することで、数値の羅列だけでは気づけないようなパターンやインサイト(洞察)を発見することを目指します。
3-1. WordCloudで頻出単語を眺める
自然言語処理のEDAで最もポピュラーな手法の一つがWordCloud(ワードクラウド) です。文章中に頻繁に出現する単語を大きく表示することで、その文章の「テーマ」を直感的に把握できます。今回は、「スパムメール」と「非スパムメール(Ham)」でそれぞれWordCloud
を作成し、使われている単語にどんな違いがあるかを見てみましょう。
# ===================================================================
# 3. データの可視化
# ===================================================================
# --- WordCloudの作成 ---
# スパムメール(label==1)の'text'列をすべて連結し、一つの巨大なテキストデータを作成
spam_text = ''.join(train[train['label']==1]['text'])
# WordCloudオブジェクトを生成。背景色などを設定し、.generate()でWordCloudを作成
spam_wc = WordCloud(background_color='white', collocations=False).generate(spam_text)
# 同様に、非スパムメール(label==0)のWordCloudも作成
ham_text = ''.join(train[train['label']==0]['text'])
ham_wc = WordCloud(background_color='white', collocations=False).generate(ham_text)
# --- グラフの描画 ---
# 描画エリアのサイズを指定
plt.figure(figsize=(10, 10))
# 複数のグラフを並べるための設定 (2行1列の1番目)
plt.subplot(2, 1, 1)
# .imshow()でWordCloud画像を表示
plt.imshow(spam_wc)
# 軸のメモリは不要なので非表示に
plt.axis('off')
# グラフのタイトルを設定
plt.title('スパムメールのWordCloud')
# 2行1列の2番目のグラフ
plt.subplot(2, 1, 2)
plt.imshow(ham_wc)
plt.axis('off')
plt.title('非スパムメール(Ham)のWordCloud')
# グラフを表示
plt.show()
この時点でのWordCloud
を見ると、まだハッキリとした特徴は掴みにくい状態です。この「まだ汚れている」状態を確認することが、次の前処理ステップへの動機付けになります。
3-2. ラベルの分布と文字数・単語数の比較
次に、より統計的な観点からデータを見ていきます。
- スパムと非スパムのメールは、それぞれ何件ずつあるのか?(ラベルの分布)
- スパムメールの長さと、非スパムメールの長さに違いはあるのか?(文字数・単語数の比較)
これらの情報をヒストグラムで比較することで、スパムメールの持つ構造的な特徴を探ります。
# --- ラベルの分布を確認 ---
plt.figure(figsize=(7, 5))
# seabornのcountplotは、指定した列の各値が何個あるかを自動で集計し棒グラフにする
sns.countplot(x='label', data=train)
plt.title('ラベルの分布 (0: Ham, 1: Spam)')
plt.grid()
plt.show()
# value_counts()で各ラベルの正確な件数を表示
print(train['label'].value_counts())
# --- 文字数と単語数を計算 ---
# .apply(lambda x: ...)` を使うと、各行のデータに対して同じ処理を一度に適用できる
# len(x)で文字数を計算
train['text_length'] = train['text'].apply(lambda x: len(x))
# x.split()でスペース区切りの単語リストを作成し、その長さを数える
train['num_words'] = train['text'].apply(lambda x: len(x.split()))
# --- ラベル別の文字数・単語数をヒストグラムで比較 ---
# 文字数の比較グラフ
plt.figure(figsize=(10, 5))
plt.hist(train[train['label']==0]['text_length'], bins=50, label='Ham', alpha=0.7)
plt.hist(train[train['label']==1]['text_length'], bins=50, label='Spam', alpha=0.7)
plt.title('ラベル別の文字数の比較')
plt.xlabel('文字数')
plt.legend()
plt.grid()
plt.show()
# 単語数の比較グラフ
plt.figure(figsize=(10, 5))
plt.hist(train[train['label']==0]['num_words'], bins=50, label='Ham', alpha=0.7)
plt.hist(train[train['label']==1]['num_words'], bins=50, label='Spam', alpha=0.7)
plt.title('ラベル別の単語数の比較')
plt.xlabel('単語数')
plt.legend()
plt.grid()
plt.show()
グラフを見ると、非スパムメール(Ham)の数がスパムメールよりも多い不均衡データであることや、スパムメールの方が文字数や単語数が多い傾向にあることが見て取れます。これらの情報は、後でモデルの精度を上げるためのヒントになります。
4. データの前処理:テキストを"お掃除"する
EDAを通して、生のテキストデータには分析のノイズとなる不要な情報が多く含まれていることがわかりました。データの前処理は、こうしたノイズを取り除き、テキストデータを機械学習モデルが学習しやすい形に整える、非常に重要な工程です。
ここで行う主な処理は以下の通りです。
-
小文字化:
Free
とfree
を同じ単語として扱えるように、全てのアルファベットを小文字に統一します。 -
ストップワード除去:
a
,the
,in
のような、文章の意味に大きく寄与しない単語を取り除きます。 -
記号・数字の除去:
re
(正規表現)ライブラリを使い、アルファベット以外の不要な文字をすべて消し去ります。
# ===================================================================
# 4. データの前処理
# ===================================================================
# --- 小文字化 ---
# .str.lower()で、'text'列の全ての文字列を一度に小文字に変換する
train['text_lower'] = train['text'].str.lower()
test['text_lower'] = test['text'].str.lower()
# --- ストップワードと記号を除去する関数を定義 ---
# NLTKライブラリから英語のストップワードリストを取得
stop_words = stopwords.words('english')
def remove_stopwords_and_symbols(text):
# 1. text.split()で、文章を単語ごとのリストに分割する
# 2. forループで各単語をチェック
# 3. if word not in stop_words で、ストップワードでない単語のみを選ぶ
# 4. re.sub('[^a-zA-Z]+', '', word) で、アルファベット(a-z, A-Z)以外の文字を消す
# 5. ' '.join(...) で、処理後の単語リストを再びスペース区切りの一つの文章に戻す
words = ' '.join([re.sub('[^a-zA-Z]+', '', word) for word in text.split() if word not in stop_words])
return words
# --- 関数を適用して、前処理済みの新しい列を作成 ---
# .apply()を使って、定義した関数を各行の'text_lower'列に適用する
train['text_processed'] = train['text_lower'].apply(remove_stopwords_and_symbols)
test['text_processed'] = test['text_lower'].apply(remove_stopwords_and_symbols)
# --- 前処理後のWordCloudを再描画して効果を確認 ---
# 前処理後のテキストでWordCloudを作成
spam_processed_text = ''.join(train[train['label']==1]['text_processed'])
spam_processed_wc = WordCloud(background_color='white', collocations=False).generate(spam_processed_text)
ham_processed_text = ''.join(train[train['label']==0]['text_processed'])
ham_processed_wc = WordCloud(background_color='white', collocations=False).generate(ham_processed_text)
# グラフ描画
plt.figure(figsize=(10, 10))
plt.subplot(2, 1, 1)
plt.imshow(spam_processed_wc)
plt.axis('off')
plt.title('【前処理後】スパムメールのWordCloud')
plt.subplot(2, 1, 2)
plt.imshow(ham_processed_wc)
plt.axis('off')
plt.title('【前処理後】非スパムメール(Ham)のWordCloud')
plt.show()
前処理後のWordCloud
を見ると、ノイズが減り、スパムメールでは"pleas", "receiv", "inform"、非スパムメールでは"enron", "vinc", "ect"といった、より特徴的な単語が浮かび上がってきました。
5. 学習・評価:AIモデルにスパムを覚えさせる
下ごしらえの済んだ食材(前処理済みテキスト)を、いよいよオーブン(機械学習モデル)に入れて調理する時が来ました。しかし、コンピュータは「文章」をそのまま理解できないため、まずテキストデータを数値のベクトルに変換する必要があります。
5-1. テキストのベクトル化 (CountVectorizer
)
CountVectorizer
は、テキストデータを数値化するための最もシンプルな手法の一つです。各メールに、どの単語が何回出現したかを数え上げ、巨大な「単語の出現回数テーブル」を作成します。
例:
- メール1: "money money click"
- メール2: "hello click"
このデータは、以下のような数値ベクトルに変換されます。
(語彙: money, click, hello)
- メール1:
[2, 1, 0]
- メール2:
[0, 1, 1]
# ===================================================================
# 5. 学習・評価
# ===================================================================
# --- 訓練データと検証データへの分割 ---
# stratify=train['label']は重要。元のデータのスパム/非スパムの比率を保ったまま分割してくれる
# test_size=0.2 は、全体の20%を検証用データとして使う設定
# random_state は、実行のたびに分割結果が変わらないようにするための固定値
X_train, X_valid, y_train, y_valid = train_test_split(
train['text_processed'], # 説明変数 (メール本文)
train['label'], # 目的変数 (正解ラベル)
test_size=0.20,
random_state=82,
stratify=train['label']
)
print(f'訓練データ数: {len(X_train)}, 検証データ数: {len(X_valid)}')
# --- CountVectorizerによる数値化 ---
# CountVectorizerのオブジェクトを作成
count_vectorizer = CountVectorizer()
# 訓練データを使って「どの単語を数えるか」の辞書を作成(fit)し、実際に数値化(transform)する
X_train_array = count_vectorizer.fit_transform(X_train).toarray()
# 検証データは、訓練データで作った辞書を再利用して数値化(transform)するだけ
# (ここでfitすると、訓練データと検証データで単語の基準がズレてしまうためNG)
X_valid_array = count_vectorizer.transform(X_valid).toarray()
# テストデータも同様にtransformする
test_array = count_vectorizer.transform(test['text_processed']).toarray()
print(f'数値化後の訓練データの形状: {X_train_array.shape}')
# (行数, 列数) -> (メール数, 辞書に登録されたユニークな単語数) となる
5-2. ナイーブベイズモデルによる学習と評価
今回は、テキスト分類で古くから使われ、シンプルながらも高い性能を発揮するナイーブベイズ分類器を使用します。ここでは、GaussianNB
とMultinomialNB
という2種類を試し、性能が良い方を採用します。
性能評価には、F1スコアという指標を使います。F1スコアは、不均衡データ(今回のようにスパムと非スパムの数が違うデータ)に対しても、モデルの性能を正しく評価しやすい指標です。
# --- 2種類のナイーブベイズモデルを準備 ---
gnb = GaussianNB()
mnb = MultinomialNB()
# --- モデルの学習 ---
# .fit(説明変数, 目的変数) でモデルにデータを学習させる
gnb.fit(X_train_array, y_train)
mnb.fit(X_train_array, y_train)
# --- 検証データで予測 ---
# .predict()で、学習済みモデルに未知のデータ(検証データ)を予測させる
gnb_valid_predict = gnb.predict(X_valid_array)
mnb_valid_predict = mnb.predict(X_valid_array)
# --- F1スコアで性能を評価 ---
# f1_score(正解ラベル, 予測ラベル) でスコアを計算する
print('--- GaussianNBのF1スコア ---')
print(f'検証データ: {f1_score(y_valid, gnb_valid_predict):.4f}')
print('\n--- MultinomialNBのF1スコア ---')
print(f'検証データ: {f1_score(y_valid, mnb_valid_predict):.4f}')
実行結果の目安
MultinomialNB
の方が高いF1スコアを示すはずです。これは、MultinomialNB
が単語の出現回数のようなカウントデータを扱うのに適したモデルだからです。
6. 予測・結果の提出:いざ、本番!
モデルの性能が良いことを確認できたので、いよいよ本番です。学習させたMultinomialNB
モデルを使い、まだ答えのわからないテストデータがスパムかどうかを予測させ、その結果をSIGNATEが指定する形式のCSVファイルに整形して提出します。
# ===================================================================
# 6. 予測・結果の提出
# ===================================================================
# --- テストデータの予測 ---
# 検証で性能が良かったMultinomialNBモデル(mnb)を使って、テストデータを予測する
mnb_pred = mnb.predict(test_array)
# --- 提出用ファイルの作成 ---
# SIGNATEからダウンロードした提出用サンプルファイルを読み込む
submit = pd.read_csv('sample_submit.csv', header=None)
# サンプルファイルの2列目(予測結果を入れる列)を、自分たちの予測結果で上書きする
submit[1] = mnb_pred
# --- CSVファイルとして保存 ---
# .to_csv()でDataFrameをCSVファイルに書き出す
# header=None, index=False は、余計なヘッダーや行番号をファイルに含めないための重要なおまけ
submit.to_csv('submission_tutorial.csv', header=None, index=False)
# --- 作成したファイルの中身を確認 ---
print('--- 作成した提出ファイル (先頭5行) ---')
print(submit.head())
print('\n提出ファイル "submission_tutorial.csv" を作成しました。')
print('Colabのファイルブラウザからダウンロードして、SIGNATEに提出してください!')
おわりに
お疲れ様でした!
これで、データの読み込みから前処理、モデルの学習、そして予測結果の提出まで、一連の流れをすべて体験することができました。作成したsubmission_tutorial.csv
をSIGNATEに提出し、自分のモデルがどれくらいのスコアを出すか、ぜひ確認してみてください。
今回のチュートリアルは、あくまでスタート地点です。
- テキストのベクトル化を
CountVectorizer
からTfidfVectorizer
に変えてみる - モデルをナイーブベイズから
ロジスティック回帰
やLightGBM
に変えてみる - 「件名」や「文字数」など、新しい特徴量を追加してみる
など、改善のアイデアは無限にあります。
この記事が、皆さんがデータ分析の世界でさらに探求を深めるための、確かな一歩となれば幸いです。