何番煎じか分からないけど集合知プログラミングをPHPでやってみた その4「はてブのリンクを推薦するシステムを作ってみる」
前回の予告で、「次回は、del.icio.usのAPIを使って、リンク推薦システム作りに挑戦です。」と書いたんですが、はてブを題材にした方が面白いかなと思って、今回は、はてブのデータを使ってリンク推薦システムを作ってみる事にしました。
データセットを作る
元書では、del.icio.usのAPIでデータを取得する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]); } ?>
実際の動作としては、以下のような感じの流れになります。
- はてブのPHPのタグ項目の上位5件のURLを取得する。
- 取ってきた5件のURLをブックマークしているユーザを30人取得する。
- 取ってきた30人がブックマークしているURLを10ページ分(200件)取得する。
- 取得したデータから、ユーザがブックマークしている場合は1.0点、していない場合は0.0点と評価しているとみなしてデータセットを作成する。
- 準備したデータセットを使って、似ているブックマーカーと、オススメのURLを計算する。
- 各ユーザの一番似ているブックマーカーと、推薦する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章の"グループを見つけ出す"というところに入っていこうと思います。