Scala ではじめる Spark / MLlib の単純ベイズ分類器

はじめに

以前のエントリにて Spark / MLlib の K-means を取り上げましたが、今回は同じく MLlib にて提供されている機械学習アルゴリズムの一つ、 単純ベイズ分類器 (Naive Bayes, ナイーブベイズ) を使ってみましょう&K-分割交差検証をしてみましょう、 というエントリです。

単純ベイズ分類器そのものについては読者の皆様はご存知、という前提でこの後の話を進めてしまいますので、 「ちょっと良くわからないよ待ってくれ!」という方は

などのページを一読することをおすすめします。

単純ベイズ分類器を利用して実現するタスクとデータ・セット

今回は、単純ベイズ分類器の応用例でよく挙げられる、スパム (迷惑メール) フィルタを MLlib を使って実現してみることにします。

スパムフィルタの訓練および評価用データは、UCI Machie Learning Repository で提供されている “SMS Spam Collection” (ショートメッセージのスパムメールデータ) を使うことにします。このデータは以下のとおり、

行頭に spam / ham のラベルが付与され、その後にタブ文字を挟んで実際のショートメッセージの文面が続きます。 このラベルの spam はスパムメール (迷惑メール) を意味し、ham は迷惑メール以外の本来受け取りたいメールを意味します。

MLlib の単純ベイズ分類器を使う手順

ではさっそく MLlib の単純ベイズ分類器を使ってみましょう。 なお今回は諸事情により、Java ではなく Scala で実装することにします。

Spark / MLlib を実行する環境を用意する

まずは Spark / MLlib を実行するための環境を作りましょう。

以前のエントリでは、 Gradle によって環境構築する方法を説明していました。 今回は Scala で実装するということもあり、Scala アプリケーションに適したビルドオートメーションの プロダクトである sbt を利用することにします。

sbt は Gradle や Maven と同様に、依存ライブラリを設定ファイルに記述・列挙することで、 自動的に必要な jar ファイルのダウンロードなどをしてくれる機能を提供しています。 Gradle で言うところの build.gradle, Maven で言うところの pom.xml に相当するファイルは、 sbt では build.sbt となります。

Spark / MLlib を利用する場合は、以下のように build.sbt に記述します (抜粋)。

NaiveBayes を利用する

続いて、MLlib の単純ベイズ分類器 NaiveBayes を利用する方法をコードで以下に示します。 (まだ慣れない Scala でコードを書いたので、Scala っぽくない記述があるやもしれませんがご了承ください)

ざっくりとした説明になってしまいますが、以下のような構成となっています。

  • ll.22 ~ 51 にて、SMS Spam Collection のデータを NaiveBayes に入力できる形式に変換しています
  • l.54 にて、交差検証を実施するためのデータ分割処理をしています (後述)
  • ll.56 ~ 71 にて、分割されたデータの組合わせごとに、訓練データで単純ベイズ分類器のモデルを構築し、 テストデータで精度を測っています

特に単純ベイズ分類器の機能、 NaiveBayes 周辺に着目してより詳細に説明をすると、以下のようになります。

  1. モデルを構築するための訓練データとして、 RDD[LabeledPoint] の RDD を用意します
  2. 上記 RDD を引数にして、 NaiveBayes.train() を呼び出すことで、訓練結果のモデルである NaiveBayesModel を得ます
  3. モデルを使って分類したいデータを Vector オブジェクトに変換し、 NaiveBayesModel#predict() に引数として渡すことで分類結果 (Double) を得ることができます

特徴ベクトルを構成する

話がちょっと前後してしまいますが、次に NaiveBayes への入力として与えられる特徴ベクトルの 構成方法について説明していきます。

今回のタスクはスパムフィルタ、ということもあって、ごく一般的な (?) Bag-of-words モデル で各ショートメールの特徴ベクトルを構成することにします。

ただここで一点、注意すべきことがあります。 Bag-of-words モデルによって構成した特徴ベクトルは疎になりがちなため、特徴ベクトルの Scala での表現方法を「密なベクトル」にしてしまうと、ヒープの利用効率が悪化してしまいます。

そのため、今回の実装では、MLlib 1.0 にて導入された Vectors.sparse() を利用して、「疎なベクトル」で特徴ベクトルを表現するようにしています。

交差検証で精度を検証する

上記で示したコードでは、データセットを訓練データとテストデータに分けて、交差検証 (クロスバリデーション) によって分類精度を算出しています。

実は MLlib では、 K-分割交差検証 を実施するためのユーティリティが提供されており、今回のコードでも当該機能を利用した交差検証を実現しています。

この K-分割交差検証を利用するのは比較的簡単で、データセットを RDD[LabeledPoint] に 変換したのちに分割数などを引数にして MLUtils.kFold() を呼び出すだけで、K 個の「訓練データ&テストデータの組み合わせ」を得ることができます。

あとは、この K 個の組み合わせデータを順に利用して、訓練データでモデルを構築し、 テストデータで精度を測る、という処理をすることになります。

しかし、便利なこの K-分割の機能、残念なことに Java からはまだ利用することができないようです…

実行結果

さてこの MLlib の NaiveBayes を使って、一体どれくらいの分類精度が得られるか、 確かめてみましょう。実行に必要なファイル一式を揃えた Git リポジトリを以下で公開していますので、 試してみたい方はこちらを git clone したのちに sbt run してみてください。

https://github.com/komiya-atsushi/mllib-naivebayes-demo

実行結果は以下のとおりです。

MLUtils.kFold() はランダムサンプリングをしているので、実行の都度、結果が微妙に変わりますが、 だいたい以下のようになります。

  • accuracy
    • 全データに対する true positive と true negative (正解) の割合
    • 98% ぐらい
  • false positive rate
    • 第一種過誤 (スパムメールじゃないのに、スパムメールと誤判定 してしまう割合)
    • 0.7 ~ 0.8% ぐらい
  • false negative rate
    • 第二種過誤 (スパムメールなのに、スパムメールじゃないと誤判定 してしまう割合)
    • 7% ぐらい

第二種過誤の割合がちょっと高めではありますが、まずまずといったところですね。

まとめ

いかがでしたでしょうか? 実装を見ても、単純ベイズ分類器を利用するのはわりと難しくないことが 分かるかと思います。 (難しい・面倒なのは、データセットを NaiveBayes の入力の形に変換する処理の実装ですね…)

また K-分割交差検証の利用も簡単にできるとあって、モデルをただ作るだけではなく ちゃんと評価できるのも嬉しい点です。

なお今回の MLlib NaiveBayes の利用デモアプリですが、Scala 実装だけではなく Java 実装 も用意していますので、興味のあるかたはぜひこちらもご覧ください。