何番煎じか分からないけど集合知プログラミングをPHPでやってみた その4「はてブのリンクを推薦するシステムを作ってみる」

前回の予告で、「次回は、del.icio.usAPIを使って、リンク推薦システム作りに挑戦です。」と書いたんですが、はてブを題材にした方が面白いかなと思って、今回は、はてブのデータを使ってリンク推薦システムを作ってみる事にしました。

データセットを作る

元書では、del.icio.usAPIでデータを取得するPythonのライブラリを使っていますが、今回は、はてブを題材にするという事で、自前で、はてブのエントリデータ取得クラスを作成しました。

その辺りのコードも全部載せていると長くなってしまうので、省略の方向で…。とりあえず、元書で使っているメソッドと同じ名前のもの(get_popular、get_urlposts、get_urlposts)を作って、余分なデータは無しで、必要なものだけ取得してくるようにしました。

このクラスがある事が前提で、データセットを作成するメソッドを作成します。今回のメソッドは、新しく、Hatenabookmarkrec.phpというファイルを作って、そちらに書いていく事にします。

<?php
/**
 * Hatenabookmarkrec
 *
 * @package
 * @version $id$
 * @copyright TOM
 * @author TOM <tom@stellaqua.com>
 * @license PHP Version 3.0 {@link http://www.php.net/license/3_0.txt}
 */

require_once(dirname(__FILE__).'/Hatenabookmark.php');

class Hatenabookmarkrec
{
    // member
    var $hatebu;

    function __construct (  )
    {
        $this->hatebu = new Hatenabookmark();
    }

    /**
     * initializeUserDict
     *
     * #test
     * <code>
     *  $methods = array('get_popular', 'get_urlposts');
     *  $mock = $this->getMock('Hatenabookmark', $methods);
     *  $return = array('url');
     *  $mock->expects($this->any())
     *      ->method('get_popular')
     *      ->will($this->returnValue($return));
     *  $return = array('tom','stellaqua');
     *  $mock->expects($this->any())
     *      ->method('get_urlposts')
     *      ->with($this->equalTo('url'))
     *      ->will($this->returnValue($return));
     *  $this->obj->hatebu = $mock;
     *  $expects = array('tom', 'stellaqua');
     *  #eq($expects, #f('PHP'));
     * </code>
     *
     * @param string $tag
     * @param int $count
     * @access public
     * @return array 該当タグのエントリリストの投稿者リスト
     */
    function initializeUserDict ( $tag, $count = 5 )
    {
        $user_dict = array();

        $populars = array_slice($this->hatebu->get_popular($tag), 0, $count);
        foreach ( $populars as $popular ) {
            $urlposts = $this->hatebu->get_urlposts($popular);
            foreach ( $urlposts as $urlpost ) {
                $user = $urlpost;
                $user_dict[] = $user;
            }
        }
        return $user_dict;
    }
    
    /**
     * fillItems
     *
     * #test
     * <code>
     *  $methods = array('get_userposts');
     *  $mock = $this->getMock('Hatenabookmark', $methods);
     *  $return = array('url');
     *  $mock->expects($this->any())
     *      ->method('get_userposts')
     *      ->will($this->returnValue($return));
     *  $this->obj->hatebu = $mock;
     *  $expects = array('tom' => array('url' => 1.0));
     *  #eq($expects,#f(array('tom')));
     * </code>
     *
     * @param array $user_dict
     * @access public
     * @return array ユーザリストを元に作られた評価データセット
     */
    function fillItems ( $user_dict )
    {
        $ratings = array();
        $all_urls = array();

        foreach ( $user_dict as $user ) {
            $posts = $this->hatebu->get_userposts($user);
            if ( $posts === false ) {
                continue;
            }
            foreach ( $posts as $post ) {
                $url = $post;
                $ratings[$user][$url] = 1.0;
                $all_urls[$url] = true;
            }
        }

        foreach ( $user_dict as $user ) {
            foreach ( $all_urls as $url => $value ) {
                if ( isset($ratings[$user][$url]) === false ) {
                    $ratings[$user][$url] = 0.0;
                }
            }
        }

        return $ratings;
    }
}
?>

Hatenabookmark.phpというのが、今回自前で作ったライブラリです。

テストはPHPUnitのモックの機能を使って、Hatenabookmarkクラスの各メソッドがごくシンプルなデータを返すようにして、必要最低限の動作確認だけしています。

ご近所さんとリンクの推薦

という事で、いよいよリンクシステム起動です。起動&結果確認用のPHPファイルを作って、動作確認してみましょう。

<?php
require_once(dirname(__FILE__).'/Recommendations.php');
require_once(dirname(__FILE__).'/Hatenabookmarkrec.php');

$rec = new Recommendations();
$hrec = new Hatenabookmarkrec();

$users = $hrec->initializeUserDict('PHP', 5);
$users = array_slice($users, 0, 30);
$user_dict = $hrec->fillItems($users);

foreach ( $users as $user ) {
    $neighbors = $rec->topMatches($user_dict, $user);
    $bookmarks = $rec->getRecommendations($user_dict, $user);

    echo "id:${user}と一番近いブックマーカー\n";
    var_dump($neighbors[0]);

    echo "id:${user}への推薦ブックマーク\n";
    var_dump($bookmarks[0]);
}
?>

実際の動作としては、以下のような感じの流れになります。

  1. はてブPHPのタグ項目の上位5件のURLを取得する。
  2. 取ってきた5件のURLをブックマークしているユーザを30人取得する。
  3. 取ってきた30人がブックマークしているURLを10ページ分(200件)取得する。
  4. 取得したデータから、ユーザがブックマークしている場合は1.0点、していない場合は0.0点と評価しているとみなしてデータセットを作成する。
  5. 準備したデータセットを使って、似ているブックマーカーと、オススメのURLを計算する。
  6. 各ユーザの一番似ているブックマーカーと、推薦するURLを表示する。

5件とか30人とか絞り込んでいるのは、そうしないとデータ量が半端なく多くなってしまう故です。

ということで、試してみた結果を以下に…。

id:mogyaと一番近いブックマーカー
array(2) {
  [0]=>
  float(0.00251798296137)
  [1]=>
  string(7) "sabotem"
}
id:mogyaへの推薦ブックマーク
array(2) {
  [0]=>
  float(1)
  [1]=>
  string(50) "http://archiva.jp/web/html-css/web-typography.html"
}
id:yocchan731と一番近いブックマーカー
array(2) {
  [0]=>
  float(0.071478977133)
  [1]=>
  string(9) "hi_marimo"
}
id:yocchan731への推薦ブックマーク
array(2) {
  [0]=>
  float(0.900247254723)
  [1]=>
  string(67) "http://coliss.com/articles/blog/wordpress/wordpress-shortcodes.html"
}
id:akishin999と一番近いブックマーカー
array(2) {
  [0]=>
  float(0.0897058936045)
  [1]=>
  string(9) "tsutomura"
}
id:akishin999への推薦ブックマーク
array(2) {
  [0]=>
  float(0.809224386987)
  [1]=>
  string(50) "http://archiva.jp/web/html-css/web-typography.html"
}
id:so_ra_toと一番近いブックマーカー
array(2) {
  [0]=>
  float(0)
  [1]=>
  string(6) "DOGEAR"
}
id:so_ra_toへの推薦ブックマーク
NULL
id:Geronimoと一番近いブックマーカー
array(2) {
  [0]=>
  float(0.032415886983)
  [1]=>
  string(10) "akishin999"
}
id:Geronimoへの推薦ブックマーク
array(2) {
  [0]=>
  float(0.934523891052)
  [1]=>
  string(52) "http://phpspot.org/blog/archives/2009/02/web_40.html"
}
 : (以下略)

結果は…妥当なんでしょうか、どうなんでしょうかね…?

結果の考察と問題点の考察

出てきた結果を見てみると、topMatchesの計算結果が0.0になってしまって、getRecommendationsで推薦できていない場合がありますね。これは多分、データの絞り込みを行っている為に、同じURLに対して1.0点を付けているユーザが1人もいない場合が出てくるせいと思われます。

とは言え、全員の全ブックマークを取得してくるのはさすがに無理なので、このくらいの結果が妥当なのかもしれません…。

あと、一つ大きな問題点がありまして…、今回、取得してくるデータを絞り込むようにしているんですが、それでもメモリ不足でFatal errorになってしまう事がしばしば…。5件とか30人という数値は、何とかそこそこのメモリで動作する、試行錯誤の賜物でございます。

これを作っている間は、「このコードが動いたら、実際に、はてブのリンク推薦サービスみたいのも作れるんじゃね?」とか思っていたんですが、そう簡単にはいかないようですね…。

実際のサービスとして作ろうと思うと、きちんと分散して計算するアルゴリズムを考えないと、データ量が増えてきた時に簡単に破綻してしまいますね。

なかなか難しいですが、奥が深くて面白いテーマですな。( ̄ー ̄)

次回は?

元書の第2章は、もう少しだけ続きがあるんですが、それは省略して、次回から第3章の"グループを見つけ出す"というところに入っていこうと思います。

余談

集合知プログラミングをやっていて、ふと「Amazonのおまかせリンクとかって、どんなもんなんだろう?」と興味が湧いて、昨晩からこのブログに設置して観察してみてたんですが、お世辞にもあまり良い推薦結果とは言い難い感じですねぇ…。

まぁ、データ量不足というのもあるのかもしれないので、もう少し様子を見てみたいと思います。