何番煎じか分からないけど集合知プログラミングをPHPでやってみた その1「ユークリッド距離によるスコアの算出」

明けましておめでとうございます。m(_ _)m 旧年中は(ry。

普通だと、ここで去年の総括とか今年の目標とか書くところなんでしょうが、1月1日だからと言ってそういうのをやるのはあまり好きではないので、通常通り"やってみた系"の記事を書いていこうかと思います。

今回ターゲットにしたテーマは?

年末に"集合知プログラミング"を買って、少しだけ読み始めたんですが、実際に自分でコード書きながら読み進めた方が、後々実際にライブラリとして使えたりしていいかなと思い、せっかくなのでブログでシリーズものとしてスタートさせる事にいたしました。

集合知プログラミング

集合知プログラミング

本書のサンプルはPythonで書かれているんですが、まがりなりにも自分はPHPerなので、PHPに書き直すようにしています。とは言え、ググってみると同じように「PHPでやってみた」というのはいくつも見つかるので、もはや何番煎じか分からないんですが(^^;、まぁ自分でやってみる事に意義があると言い聞かせて、突き進む事にします。

早速やってみよう!

今回は、一番最初のサンプルの、"ユークリッド距離によるスコア"(2.3.1)の関数を作ってみました。

まずはサンプルデータを変数に代入する部分を作ります。

<?php
$critics = array(
'Lisa Rose' =>
    array('Lady in the Water' => 2.5,
          'Snakes on a Plane' => 3.5,
          'Just My Luck' => 3.0,
          'Superman Returns' => 3.5,
          'You, Me and Dupree' => 2.5,
          'The Night Listener' => 3.0,
         ),
'Gene Seymour' =>
    array('Lady in the Water' => 3.0,
          'Snakes on a Plane' => 3.5,
          'Just My Luck' => 1.5,
          'Superman Returns' => 5.0,
          'The Night Listener' => 3.0,
          'You, Me and Dupree' => 3.5,
         ),
'Michael Phillips' =>
    array('Lady in the Water' => 2.5,
          'Snakes on a Plane' => 3.0,
          'Superman Returns' => 3.5,
          'The Night Listener' => 4.0,
         ),
'Claudia Puig' =>
    array('Snakes on a Plane' => 3.5,
          'Just My Luck' => 3.0,
          'The Night Listener' => 4.5,
          'Superman Returns' => 4.0,
          'You, Me and Dupree' => 2.5,
         ),
'Mick LaSalle' =>
    array('Lady in the Water' => 3.0,
          'Snakes on a Plane' => 4.0,
          'Just My Luck' => 2.0,
          'Superman Returns' => 3.0,
          'The Night Listener' => 3.0,
          'You, Me and Dupree' => 2.0,
         ),
'Jack Matthews' =>
    array('Lady in the Water' => 3.0,
          'Snakes on a Plane' => 4.0,
          'The Night Listener' => 3.0,
          'Superman Returns' => 5.0,
          'You, Me and Dupree' => 3.5,
         ),
'Toby' =>
    array('Snakes on a Plane' => 4.5,
          'You, Me and Dupree' => 1.0,
          'Superman Returns' => 4.0,
         ),
);
?>

何だかいかにも打ち間違いを誘発しそうな書き方で、全然スマートじゃないのが激しく気にはなるんですが…まぁ、サンプルデータを定義するだけなのでこれでよしとしましょう。*1

これを、"setcritics.php"という名前で保存して、サンプルでテストする時にrequireして使う事にします。

さぁ実装だ!…でもその前に…

サンプルデータの準備もできて、いよいよ実装に入る訳ですが、実装を始める前にやるべき事があるよね?…そう、プロテインだね。…じゃなくて、そう、テストを書くんだね! "実装の前にはまずテスト"、TOM先生との約束だよ! という事で(?)、テストを書いてから実装をしていく事にしましょう。*2

では、クラスの宣言とメソッドの宣言を書いて、テストを書いてみましょう。

<?php
/**
 * Recommendations
 *
 * @package
 * @version $id$
 * @copyright TOM
 * @author TOM <tom@stellaqua.com>
 * @license PHP Version 3.0 {@link http://www.php.net/license/3_0.txt}
 */
class Recommendations
{
    /**
     * sim_distance
     *
     * #test 同じ人の相関性は1
     * <code>
     *  require('./setcritics.php');
     *  #eq(1.0, #f($critics, 'Lisa Rose', 'Lisa Rose'));
     * </code>
     *
     * #test 正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $d =0.000000000001;
     *  #eq(0.294298055086, #f($critics, 'Lisa Rose', 'Gene Seymour'), '', $d);
     * </code>
     *
     * @param mixed $prefs
     * @param mixed $person1
     * @param mixed $person2
     * @access public
     * @return void
     */
    function sim_distance ( $prefs, $person1, $person2 )
    {
    }
}

テストは別のファイルに書いてたりすると面倒なので、DocTestで作成するclassの中に書いていきます。

本当は、もっと細かいレベルでテストを書いていった方がいいとは思うんですが、ちょっと手抜きをして、本書に載っていた実行結果サンプルをいきなりテストとして書いています。また、正常動作確認は、丸め誤差が発生する為、誤差の許容範囲を指定するようにしています。

実装完了!

今回はTDDの話ではないので、途中の過程はすっ飛ばして、できあがったコードだけを載せたいと思います。

<?php
/**
 * Recommendations
 *
 * @package
 * @version $id$
 * @copyright TOM
 * @author TOM <tom@stellaqua.com>
 * @license PHP Version 3.0 {@link http://www.php.net/license/3_0.txt}
 */
class Recommendations
{
    /**
     * sim_distance
     *
     * #test 同じ人の相関性は1
     * <code>
     *  require('./setcritics.php');
     *  #eq(1.0, #f($critics, 'Lisa Rose', 'Lisa Rose'));
     * </code>
     *
     * #test 正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $d =0.000000000001;
     *  #eq(0.294298055086, #f($critics, 'Lisa Rose', 'Gene Seymour'), '', $d);
     * </code>
     *
     * @param mixed $prefs
     * @param mixed $person1
     * @param mixed $person2
     * @access public
     * @return float 相関値
     */

    function sim_distance ( $prefs, $person1, $person2 )
    {
        $si = array();
        foreach ( $prefs[$person1] as $item => $value ) {
            if ( array_key_exists($item, $prefs[$person2]) ) {
                $si[$item] = 1;
            }
        }

        if ( count($si) === 0 ) {
            return 0.0;
        }

        $sum_of_squares = 0;
        foreach ( $prefs[$person1] as $item => $value ) {
            if ( array_key_exists($item, $prefs[$person2]) ) {
                $sum_of_squares +=
                    pow($prefs[$person1][$item] - $prefs[$person2][$item], 2);
            }
        }

        return 1.0 / ( 1.0 + sqrt($sum_of_squares) );
    }

}
?>

とりあえず、元のPythonのコードのベタ移植で進めていこうと思っているので、あんまりスマートに書こうとかは考えてないです。

という訳でテストを実行してみると無事に通ったので、問題なく実装できたようです。

今後は?

次回以降も、本書に載っているサンプルを順番にPHPで実装していくのをボチボチ進めていきたいと思います。ただ、それだけだと飽きてきてしまうので、ある程度アルゴリズムを把握したものは、実際のサービスに適用したものを作ったりもしていこうかと思っています。

あとがき

冒頭で「正月に目標を決めたりとかは好きじゃない。」というような事は書いたものの、何だかんだ言っても1月1日に多少なりとも特別な意味を感じてしまうというプレッシャーは避けがたいので(^^;ゞ、1つだけ小さな目標というか、やっていきたいなと思う事を挙げようと思います。

年末に、"gitを触ってみるよ"と"AtomPubな何かを作ってみよう"というのをシリーズものとして始めてみたんですが、今年はこういった連載っぽい感じの記事を、種類を増やして書いていきたいなと思っています。

今後もシリーズものが増えていった時にそれぞれのシリーズが検索し易くなるように、過去の記事のタイトルに"シリーズx"というタグを追加しました。*3

そんな訳で、今年もどうぞよろしくお願いいたします。m(_ _)m

*1:実際、一箇所打ち間違いをしてしまって、後で本書に載っている実行結果と違う結果になってしまって、困ったりしました。良い子のみんなは、もうちょっとスマートな書き方をしようね。(笑)

*2:TDDに関しては、"TOM先生のテスト講座"で詳しく話しています。そちらもぜひ見てみて下さいね。(宣伝)

*3:最初、"AtomPubな何かを作ってみようシリーズ"とかタグを付けたら、カテゴリ選択欄がボックスを突き抜けてしまい、やむなく単純なタグにする事に…。カテゴリの検索性はちょっと考えたいところですな…。