30分でできる分散レコメンデーション:パラメータを変更して、応用できるようにする。

仕事が忙しくて、更新に間があいてしまった。

前々回前回のログApache Mahout0.7で実装されている「Parallel ALS (Parallel Alternating Least Squares)アルゴリズム」をつかって、とても簡便にスケーラブルなリコメンデーションエンジンを構築してみた。
実行には、Amazon Elastic MapReduceのm1.mediumのインスタンスを使用した。Mahoutに付属するサンプルジョブは、m1.smallでも実行できるが、Hadoopの実行要件を考えれば、m1.medium以上で実行するのが適当。実際、「30分で構築するレコメンデーションエンジン」で利用したLibisetiデータのスケールでは、m1.smallを使うと、Heapが不足してジョブがエラーになってしまう。

さて、Libisetiのデータでは、λの値として0.20を選択しており、その選択の根拠を前回のログでは記載した。

今回は、連載の最後として、例示したサンプルをカスタマイズする方法を記しておこうとおもう。


実際に、「30分で構築するレコメンデーションエンジン」のジョブは、以下の要件さえみたせば、全く違ったデータに活用できる。ジョブもスケーラブルに実行されるので、データ量による束縛もない。

  • プリファレンスデータが、「ユーザーID(数値),アイテムID(数値),プリファレンス値(数値)」のフォーマットのCSV形式であること。

上の要件は、Mahoutの協調フィルタリングで利用される「標準フォーマット」であればよい、ということ。データの量や密度(ユーザー×アイテムの行列に対して、どの程度のプリファレンス値が与えられているのか)によって、実行時間や、Hadoopクラスタのスケールが決まってくるが、基本的には上記の1つの要件さえ満たせばいいのは、協調フィルタリングの最大のメリット。しかも、スケーラブルなので、試してみない手はない、といった代物だと思う。

要件は1つだけ、と言ったばかりだが、実際のケースでは、以下のようにジョブ(以下のfactorize-libimseti.sh)を修正する必要がある。

  1. 適切なλを決定する。
  2. 適切なFeature数を決定する。
  3. シェルにハードコードされている、プリファレンス値の上限値を変更する。
if [ "$1" = "--help" ] || [ "$1" = "--?" ]; then
  echo "This script runs the Alternating Least Squares Recommender on the Groupl
ens data set (size 1M)."
  echo "Syntax: $0 /path/to/ratings.dat\n"
  exit
fi

if [ $# -ne 1 ]
then
  echo -e "\nYou have to download the libimseti dataset from http://www.occamslab.com/petricek/data/  before"
  echo -e "you can run this example. After that extract it and supply the path to the ratings.dat file.\n"
  echo -e "Syntax: $0 /path/to/ratings.dat\n"
  exit -1
fi

MAHOUT="../../bin/mahout"
hadoop fs -mkdir /usr/hadoop/tmp/libimseti
hadoop fs -copyFromLocal $1 /usr/hadoop/tmp/libimseti
WORK_DIR=/usr/hadoop/tmp

# create a 90% percent training set and a 10% probe set
$MAHOUT splitDataset --input ${WORK_DIR}/libimseti/ratings.dat --output ${WORK_DIR}/dataset  --trainingPercentage 0.9 --probePercentage 0.1 --tempDir ${WORK_DIR}/dataset/tmp

# run distributed ALS-WR to factorize the rating matrix defined by the training 
set
$MAHOUT parallelALS --input ${WORK_DIR}/dataset/trainingSet/ --output ${WORK_DIR}/als/out  --tempDir ${WORK_DIR}/als/tmp --numFeatures 20 --numIterations 10 --lambda 0.200

# compute predictions against the probe set, measure the error
$MAHOUT evaluateFactorization --input ${WORK_DIR}/dataset/probeSet/ --output ${WORK_DIR}/als/rmse/  --userFeatures ${WORK_DIR}/als/out/U/ --itemFeatures ${WORK_DIR}/als/out/M/ --tempDir ${WORK_DIR}/als/tmp 

# compute recommendations
$MAHOUT recommendfactorized --input ${WORK_DIR}/als/out/userRatings/ --output ${WORK_DIR}/recommendations/ --userFeatures ${WORK_DIR}/als/out/U/ --itemFeatures ${WORK_DIR}/als/out/M/  --numRecommendations 6 --maxRating 10

# print the error
echo -e "\nRMSE is:\n"
hadoop fs -copyToLocal /usr/hadoop/tmp/als/rmse/rmse.txt .
cat rmse.txt
#cat ${WORK_DIR}/als/rmse/rmse.txt
echo -e "\n"

echo -e "\nSample recommendations:\n"

hadoop fs -copyToLocal /usr/hadoop/tmp/recommendations/part-m-00000 .
shuf part-m-00000 |head
#shuf ${WORK_DIR}/recommendations/part-m-00000 |head
echo -e "\n\n"

echo "removing work directory"
rm -rf ${WORK_DIR}

適切なλを決定する。

Prallel ALSの原論文「Large-scale Parallel Collaborative Filtering for the Netflix Prize」では、(Netflix Prizeのデータを使った場合)λ=0.065が最適な値、と記されている。Mahoutに付属する(1MBのMovie Lensデータを使った)サンプルでも、λの値は0.065に設定されている。
だが、先のログで示したように、Libisetiのデータでは、繰り返し実験を行うことで、0.2を最適値としt採用した。このように、実際の現場では、λの値を実験により決定することが必要となってくる。この場合には、Hadoopのマスタノードにログインして、以下のようにして「作成済みのデータを消し」、λの値を変えて再度ジョブを実行すればよい。

rm -fr part-m-00000 
rm -fr rmse.txt
hadoop fs -rmr /usr/hadoop/tmp/*

λは、factorize-libimseti.shにある「--lambda 」の値を書き換えればよい。

# run distributed ALS-WR to factorize the rating matrix defined by the training 
set
$MAHOUT parallelALS --input ${WORK_DIR}/dataset/trainingSet/ --output ${WORK_DIR}/als/out  --tempDir ${WORK_DIR}/als/tmp --numFeatures 20 --numIterations 10 --lambda 0.200

適切なFeature数を決定する。

Feature数もパラメータの1つで、これも適切に設定する必要がある。
この際に注意することは、Featuresのサイズは「スケーラブルではない計算アルゴリズム」によって行われる「行列積と逆行列計算の実行時間」に直接的に影響を及ぼしてしまうこと。これについても、以前のログに記載したが、行列積の算術演算の回数は(仮に正方行列同士の演算と仮定すると)行数(=列数)の3乗のオーダーとなる。Featuresのサイズは、この行数にあたるので、20を30にすると、1.5^3=3.375倍の演算が必要となる。
先のログに示したように、Libisetiのデータでは、featuresの数を30にすることで、逆にRMSEの値が悪くなった。featuresのサイズは「潜在的な変数」的にもとらえられるが、大きくすれば良い結果がでるというものでもないし、演算回数(実行時間)との兼ね合いも含めて吟味する必要がある。

これを変更する場合には、factorize-libimseti.shにある「--numFeatures 」の値を書き換えればよい。

# run distributed ALS-WR to factorize the rating matrix defined by the training 
set
$MAHOUT parallelALS --input ${WORK_DIR}/dataset/trainingSet/ --output ${WORK_DIR}/als/out  --tempDir ${WORK_DIR}/als/tmp --numFeatures 20 --numIterations 10 --lambda 0.200

シェルにハードコードされている、プリファレンス値の上限値を変更する。

これは「プリファレンスの上限値はいくつか」を明示するパラメータで、actorize-libimseti.shにある「--maxRating」の値を書き換えればよい。Libisetiのデータでは最高の評価が10なので、10と設定するが、Movie Lensのように5が最大値なら5と設定する。

$MAHOUT recommendfactorized --input ${WORK_DIR}/als/out/userRatings/ --output ${WORK_DIR}/recommendations/ --userFeatures ${WORK_DIR}/als/out/U/ --itemFeatures ${WORK_DIR}/als/out/M/  --numRecommendations 6 --maxRating 10


さて、なんで「評価の最大値なんて設定するのだろう?」と思わなかっただろうか? この値は、レコメンドの推定に関係する。Parallel ALSでは、以前のログ「Apache Mahoutの分散次元縮約(Parallel ALS)を解説しよう」に記載したように、以下の式によってプリファレンス(レーティング)値を推定する(ジョブの中で10%のトレーニングをつかってRMSEを算出する際にもこの式が用いられる)。

この際、推定されたプリファレンス値が10を超える可能性がある。ここで指定する10という数字は、推定したプリファレンス値が10を超えたら(形式上)10にするよ、という目的で設定する。


さぁ、これで、データを準備すれば自前でレコメンデーションエンジンが構築できると思う。
くどい感は否めないが、MahoutのParallel ALSをこのように応用することで、

  1. 協調フィルタリングによる幅広い応用(上記の形式のデータがあればよい)
  2. スケーラブルなレコメンデーションエンジンの構築(分散処理されるために、1億件のデータがあっても処理できるだろう)
  3. Amazon Elastic MapReduceを使った「自前でサーバーを持たない」ローコスト運用

が可能になる。