何番煎じか分からないけど集合知プログラミングをPHPでやってみた その3「アイテムを推薦する・似ている製品」

前回は、評者同士の類似度を計算する関数を使って、評者のランキングを返すメソッドの作成までやりました。今回は、いよいよ(ようやく?)実際に推薦を行うメソッドを作成してみたいと思います。

アイテムを推薦する

基本的な考え方としては、自分が評価していないアイテムに関して、他の評者の評点を合計して、点数の高いモノほどオススメ…という感じですね。ただ、もうひと捻りあって、評者同士の類似度を計算する関数を利用して、自分と似ている人ほど評点が高くなるように重み付けされるようにしていますね。

では、実際にコードを書いてみましょう。例によって、Recommendations.phpに追加します。

<?php
class Recommendations
{
    /**
     * getRecommendations
     *
     * #test 正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $expected = array(
     *      array(3.3477895267131013, 'The Night Listener'),
     *      array(2.8325499182641614, 'Lady in the Water'),
     *      array(2.5309807037655645, 'Just My Luck'),
     *      );
     *  $d =      0.000000000000001;
     *  #eq($expected, #f($critics, 'Toby'), '', $d);
     * </code>
     *
     * #test sim_distance関数を指定した場合の正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $expected = array(
     *      array(3.5002478401415877, 'The Night Listener'),
     *      array(2.7561242939959363, 'Lady in the Water'),
     *      array(2.4619884860743739, 'Just My Luck'),
     *      );
     *  $d =      0.000000000000001;
     *  #eq($expected, #f($critics, 'Toby', 'sim_distance'), '', $d);
     * </code>
     *
     * @param mixed $prefs
     * @param mixed $person
     * @param string $similarity
     * @access public
     * @return array 推薦結果
     */
    function getRecommendations ( $prefs, $person, $similarity = 'sim_peason' )
    {
        $totals = array();
        $simSums = array();
        foreach ( $prefs as $other => $value ) {
            if ( $other === $person ) {
                continue;
            }
            $sim = $this->$similarity($prefs, $person, $other);

            if ( $sim <= 0.0 ) {
                continue;
            }

            foreach ( $prefs[$other] as $item => $value ) {
                if ( isset($prefs[$person][$item]) === false
                     || $prefs[$person][$item] === 0.0 ) {
                    $totals[$item] += $prefs[$other][$item] * $sim;
                    $simSums[$item] += $sim;
                }
            }

            $rankings = array();
            foreach ( $totals as $item => $total ) {
                $rankings[] = array(
                                    $total / $simSums[$item],
                                    $item
                                   );
            }
            usort($rankings, array('Recommendations', '_multicmp'));
        }
        return $rankings;

    }

    function _multicmp ( $a, $b )
    {
        if ( $a[0] === $b[0] ) {
            return 0;
        }
        return ( ($a[0] < $b[0]) ? 1 : -1 );
    }
}
?>

テストで書いている通り、Tobyさんにオススメの映画を出力させてみると、"The Night Listener"が一番オススメなようですね。

似ている製品

さて、今は評者同士の類似度を使って、ある評者にアイテムを推薦するメソッドを作成しました。今度は視点を変えて、アイテム同士の類似度を計算して、あるアイテムに関する評者をオススメするようにしてみましょう。

これをする為に、今まで評者ベースになっていたデータを、アイテムベースに変換するメソッドを作成します。

<?php
class Recommendations
{
    /**
     * transformPrefs
     *
     * #test 正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $result = #f($critics);
     *  #eq(2.5, $result['Lady in the Water']['Lisa Rose']);
     *  #eq(3.0, $result['Lady in the Water']['Gene Seymour']);
     *  #eq(3.5, $result['Snakes on a Plane']['Lisa Rose']);
     *  #eq(3.5, $result['Snakes on a Plane']['Gene Seymour']);
     * </code>
     *
     * @param mixed $prefs
     * @access public
     * @return array 変換後のディクショナリ
     */
    function transformPrefs ( $prefs )
    {
        $result = array();
        foreach ( $prefs as $person => $items ) {
            foreach ( $items as $item => $value ) {
                $result[$item][$person] = $prefs[$person][$item];
            }
        }
        return $result;
    }
}
?>

これで、"あるアイテムに対して、誰が何点付けているか"というアイテムベースの持ち方のデータを使う事ができます。

データの形式自体は変わっていなくて、データの意味だけが変わっただけなので、以前作成したtopMatches()や、今回作成したgetRecommendations()もそのまま利用する事ができます。

topMatchesのテストに、アイテムベースのデータを使ったテストを追加しましょう。

<?php
    /**
     * topMatches
     *
     * #test アイテムに関する相関度を取得する確認
     * <code>
     *  require('./setcritics.php');
     *  $expected = array(
     *      array( 0.657951694960, 'You, Me and Dupree'),
     *      array( 0.487950036474, 'Lady in the Water'),
     *      array( 0.111803398875, 'Snakes on a Plane'),
     *      array(-0.179847194799, 'The Night Listener'),
     *      array(-0.422890031611, 'Just My Luck'),
     *  );
     *  $d =       0.000000000001;
     *  $movies = $this->obj->transformPrefs($critics);
     *  #eq($expected, #f($movies, 'Superman Returns'), '', $d);
     * </code>
     */
?>

これで、"Superman Returns"を好きな人は"You, Me and Dupree"も好きで、逆に"Just My Luck"は嫌いだ…なんて事が分かるようになります。

続いて、getRecommendationsの方もテストを追加してみましょう。

<?php
    /**
     * getRecommendations
     *
     * #test アイテムベースで評者を推薦する場合の正常動作確認
     * <code>
     *  require('./setcritics.php');
     *  $expected = array(
     *      array(4.0, 'Michael Phillips'),
     *      array(3.0, 'Jack Matthews'),
     *      );
     *  $d = 0.1;
     *  $movies = $this->obj->transformPrefs($critics);
     *  #eq($expected, #f($movies, 'Just My Luck'), '', $d);
     * </code>
     */
?>

この結果がどういう意味を持つのかという意味付けは若干微妙なところはあるんですが…サービス提供者の視点で、この映画をまだ評価していない人に対して"こんな映画もありますよ"というようなメッセージを送ったりするのに使えるかもしれませんね。

次回は?

今回までで、推薦に必要なメソッドの作成を行ってきた訳ですが、いかんせんサンプルデータを使ったテストだけだったのでリアリティに欠けた感が若干ありました。次回は、del.icio.usAPIを使って、リンク推薦システム作りに挑戦です。