mahoutで類似ユーザをリコメンドする

mahout使って類似ユーザの抽出をやってみたのでまとめ。

そこら辺に転がってるサンプルは、ほとんどがユーザがあるアイテムについて点数をつける前提になってて、その点数をベースに似ているユーザをリコメンドしたり、あるいはあるユーザが興味をもちそうなアイテムをリコメンドしたりといったものが殆どだった。点数がないようなときにどうするか多少悩んだので記録に残しておくことにした。とりあえず今回はMySQLにデータが入ってる前提でやってみた。あるテーブルにUSER_IDとITEM_IDが入ってて、同じITEM_IDを多く選んだ人を探したいとすると、以下のようにする。

package test;

import test.DataSouceManager;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.model.jdbc.MySQLBooleanPrefJDBCDataModel;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.LogLikelihoodSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;

public class RecommendTest {
	public static void main(String[] args) {
		DataModel model = new MySQLBooleanPrefJDBCDataModel(
			DataSourceManager.getInstance().getDataSouce(),
			"TABLE_NAME",
			"USER_ID_COLUMN_NAME",
			"ITEM_ID_COLUMN_NAME",
			null //TIMESTAMP_COLUMN_NAME
		);
		UserSimilarity similarity = null;
		UserNeighborhood neighborhood = null;
		try {
			similarity = new LogLikelihoodSimilarity(model);
			neighborhood = new NearestNUserNeighborhood(100, similarity, model);
		} catch (TasteException e) {
			e.printStackTrace();
		}
		GenericUserBasedRecommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
		try {
			long[] recommendations = recommender.mostSimilarUserIDs(1, 1);
			for (int i = 0; i < recommendations.length; i++) {
				resp.getWriter().println(recommendations[i]);
			}
		} catch (TasteException e) {
			e.printStackTrace();
		}
	}
}

統計学の知識がないのでドキュメント読んでもよくわからなかったので、これであってるのかどうかはよくわからん。色々試して、LogLikelihoodSimilarityじゃないと点数のフィールドがねーから使えないよと怒られた。GenericUserBasedRecommenderはRecommenderってIFで受けるのが筋なんだろうけど、そうするとmostSimilarUserIDSメソッドが使えないのでそのままの型で受けてる。

mostSimilarUserIDsの第一引数が比較したいユーザID、第二引数が何人リコメンドするか。少ないデータで試して、期待する結果が返ってくるのは確認済み。んで大量のデータだとどうなんだろうと思って、以下のスクリプトで140万件のデータを作ってみた。

日付・ユーザID・アイテムIDで、ユーザIDは一日につき一回しかでてこないようにデータを生成。

import string
import random
import sys

if __name__ == '__main__':
  for d in xrange(20110201, 20110229):
    idmap = []
    for i in xrange(50000):
      while(1):
        id = random.randint(1,300000)
        try:
          idmap.index(id)
        except ValueError:
          break
        continue
      idmap.append(id)
      print "insert into tbl values ('" + str(d) + "'," + str(id) + "," + str(random.randint(1,3*28)) + ");"

こいつでSQLをつくってMySQLに食わせる。

$ python generate_date.py|mysql

MEMORYエンジンでテーブル作って試してみたけど、DataModelをつくるところですでに時間掛かりすぎ。リアルタイムで返事を返すようなときには全然向いてないものみたいで、やっぱHadoopとかを併用して裏で動かしておく感じで使う物みたいだ。ちなみにかかった時間はおよそ10分弱。こんな単純なデータで単純なことやりたいのであれば、mahoutじゃなくて素直にSQLでやったほうが速い。もっともっと大規模なデータを解析するのに使わないと。