Scala はじめました 〜 cross validation を用いた単純パーセプトロンの評価を実装 〜

ご無沙汰しております。システム開発部の potter です。

Jubatus のバースト検知を使ってみた話以来となるので久々の投稿となります。

最近運用チームから開発チームに異動したのですが、Scala を使うプロジェクトがありそうということでその勉強を始めました。

今回はそんな Scala 歴 2 週間足らずの私が人生で初めて書いた Scala プログラムを晒してしまおうという、ちょっぴり M な香りのする企画となります。 (ただし晒しているのは Scala の先輩であるシステム開発部の uraura 先生にいくつかアドバイスを頂いて修正したプログラムになります。Special tanks to uraura!!

作成したプログラムの内容

単純パーセプトロンによる識別精度を n-fold cross validation により評価するプログラムを作成しました。1

使用する入力データは下記のような3カラムのファイルとなります。

  • 1列目:特徴ベクトルの1次元目
  • 2列目:特徴ベクトルの2次元目
  • 3列目:正解ラベル

入力データの特徴ベクトルをグラフにプロットすると下記のようになります。

データ数は100です。特徴ベクトルの 1 次元目を横軸に、特徴ベクトルの 2 次元目を縦軸にとり、正解ラベルが -1 のデータは青い点で、正解ラベルが 1 のデータは赤い点で示しています。

scala-perceptron_data

今回は 10-fold cross validation としましたので、これらのデータを10グループに分割します。

その中の9グループ(すなわち90個のデータ)を学習データとして用い、データを分類する識別直線を学習します。

その後残りの1グループ(すなわち10個のデータ)を評価データとして用い、その識別直線で正しく識別できるかを評価します。

学習データ・評価データの選び方は10通りあるので、この学習・評価を10回繰り返して総合的な精度を算出します。

ソースコード

今回は sbt プロジェクトとしてプログラムを作成しました。ソースのフォルダ階層は以下の通りです。2

scala-perceptron_project

各プログラムのソースは以下の通りです。

build.sbt

Main.scala

Perceptron.scala

実行結果の確認

プログラムを実行すると、標準出力に下記の通り出力されます。

妥当な結果が得られているのか確認するため、 evaluationGroup = 1 の結果について見てみましょう。

まず、 evaluationGroup = 1 の結果では下図の 90 個の学習データによる学習によって(-0.02,-0.06260001,0.10020004)という重みベクトルが得られています。

この重みベクトルは、下図の緑色の識別直線でデータを識別することを意味します。

識別直線付近の際どいデータもありますが、学習データについては誤分類が発生しないような識別直線が得られ、学習が収束していることがわかります。

scala-perceptron_training

次に、上記の学習で得られた識別直線と評価データをプロットしたのが下図になります。

ご覧の通り、赤い点が一つだけ識別直線の下に配置されていることがわかります。

このデータを誤識別してしまったために accuracyRate=0.9 となっていることがわかります。

scala-perceptron_evaluation

Scala 力をアップするために

今回、構文すら良くわかっていない状態からプログラムを書き始めたのですが、 やはり最初はどのように記載するのが Scala らしいプログラムなのかといったことも良くわからず、試行錯誤でした。

そのような中で、私と同じような Scala 初学者がどういったことに気を付ければ Scala 力をアップして行けるのか、 私なりに感じたことを記載させて頂きます。

map や filter などの予め用意されている高階関数の使い方を覚えよう

Scala はオブジェクト指向型と関数型の 2 つのパラダイムを兼ね備えた言語となります。 後者の関数型のパラダイムに由来する部分ですが、Scala の関数は「第一級の値」として扱われます。 これは IntString を関数の引数や戻り値にできるように、関数を関数の引数や戻り値にすることができることを意味します。

mapfilter といったよく使われる関数も引数に関数を取る関数(高階関数)となります。 これらの関数を有効に使えるようになってくると、プログラムもすっきりしてくるし、関数を引数に取るという考え方にも慣れてくるように思います。

個人的には、これらの関数を使う際は引数となっている関数の入力(引数)と出力(戻り値)の型を意識すると理解しやすいように思いました。

immutable な変数を使って処理を書くことに慣れよう

一度値を設定した変数に再代入することができないことを immutable であると言います。 Scala では val で定義した変数は immutable な変数、 var で定義した変数は mutable な変数となります。

Scala では(初期設定された値が変更されないことが保証されるので)なるべく mutable な変数は用いずに処理を書くことが推奨されます。

そのような中で Java などとは少し異なる方法で処理の実装を検討する場面が出てきます。

今回のプログラムでも、 createSameValueList を、末尾再帰の再帰関数3として定義しています。関数型プログラミングに馴染みのなかった私などは下記のような実装をしたくなってしまうところですが、この場合 mutable な変数を用いることになってしまうので上述したソース内の実装としました。

その他、 totalEvaluationCounttotalCorrectCount についても、 mutable な変数として定義して for 文で足し込んで行くという実装も出来そうですが、 foldLeft 関数を用いて畳み込むことで mutable な変数の利用を避けています。

ただ必ずしも immutable な変数のみで処理を記載しなければならないというわけではありませんし、 mutable な変数を用いなければならない(又は用いた方が良い)局面もあるかと思います。

immutable な変数での処理記述のパターンを理解し、どういった時に immutable に拘るべきなのか、 どういった時には mutable の変数を用いるべきなのかを判断できるようになった方が良いかと思いますが、このあたりは少し経験が必要なのかなと感じました。

なお、余談となりますが、最初に末尾再帰で createSameValueList 関数を実装しようとした時に下記のような実装をしていました。

しかしこれは末尾再帰になっていません。 createSameValueList の評価の後に elementValue をリストに追加する :: が評価されるためです。ソースの最後に記載しているからといって末尾再帰になるわけではなく、あくまで関数内で最後に評価されないと末尾再帰にならないということですね。

ここは uraura 先生からの有り難いマサカリによって気づいた所です。同時に 「 @tailrec を指定して末尾再帰になっていなかったらコンパイルエラーになるようにしようね」というアドバイスも頂きました。

Scala 特有の構文を覚えよう

Scala にはプログラムの見通しを良くするために便利な構文が存在しています。

例えば私は最初 DTO として case classを定義できることや、クロージャで tuple を入力とする際 case (x, y)のように記述できることを知らなかったのですが、これらを使うことでだいぶプログラムの見通しが良くなったと感じています。このあたりも uraura 先生に頂いたアドバイスで改善した所です。

こういった有効な構文を 1 つずつ覚えて使えるようになっていくと、 Scala 力がアップしていくのかなと思いました。

参考書籍

今回は下記のような書籍を参考にして勉強・実装を進めました。

まとめ

  • Scala を用いて単純パーセプトロンによる識別を n-fold cross validation により評価するプログラムを作成しました。
  • 作成したプログラムの実行結果によって学習・評価がどのように実施されているかを確認しました。
  • Scala 初学者が Scala 力をアップさせるために意識した方が良いことについて考えてみました。

今回自分にとって未経験の言語にチャレンジしてみたわけですが、やはり新しいことにチャレンジするというのは楽しいものですし、とりわけ Scala は勉強していて楽しい言語だなと感じました。

またプログラムを勉強する際、何かしらのプログラムを実際に作成してみる事は大きな勉強になると改めて思いました。今回パーセプトロンのプログラムを実装してみて、ようやく知識を肉付けて行くための 1 つの軸ができたかなと思います。

とは言え今回のプログラムでは出てきていない概念もありますし、勉強することはまだまだありますので、さらに精進していこうと思った次第です。

ではでは、今回はこのあたりで失礼いたします!


  1. 本投稿では単純パーセプトロンのアルゴリズムについての説明は行いません。インターネット上に参考になる資料が既に多数存在しますが、例えばこちらで公開されている資料などは参考になるかと思います。 

  2. 今回は intellij の Scala プラグインにおける sbt テンプレートでプロジェクトを作成しました。 

  3. 末尾再帰とは関数の最後で自分自身を呼び出す再帰のことを意味します。Scala においては再帰関数を定義する際は末尾再帰で記述することが推奨されています。参考書籍を読んで理解したのみですが、末尾再帰にすることで呼び出し前のスタック状態を保持する必要が無くなってコンパイラによる最適化時に単純なループに変換することができるようになり、スタックオーバーフローのリスクや再帰による実行時のオーバーヘッドがなくなるとのことです。