Hadoopでレコメンドシステムを作ろう
第3回 レコメンドシステム-協調フィルタリングのHadoopへの実装[前編]
今回はいよいよHadoopを用いたレコメンドシステムについて説明します。
今回のポイントは以下の通りです。
- 処理をMapReduceフレームワークへ変換することで,
分散処理のメリットを享受 - アウトプットからkeyについて着目し,
処理ロジックを考える - 簡単な処理でも数段階のMapReduce処理を踏む場合がある
前回までのおさらい
分散処理の基本的な考え方は,
Hadoopで処理するため,
前回紹介したように,
そこで単純に考えると,
ですが,
欲しいアウトプットはアイテム間のコサイン関数による値なので,
cos(a,b)=a*b/|a||b| =(a1*b1+a2*b2+a3*b3+....an*bn)/(√a1^2+a2^2+a3^2+....an^2)(√b1^2+b2^2+b3^2+....bn^2)
するとa1b1,...
今までの処理ロジックを分散環境でそのまま実行するだけでなく,
結果から逆に考える
コサイン関数を計算するために,
この図において,
この図より,
ここでMapperおよびReducerの設計に使えるポイントをまとめます。これらのポイントはこのコサイン関数だけでなく,
- Mapperは入力データからkeyとvalueを入力データから指定して抽出するだけでなく,
内部で抽出したデータに対して変換や演算処理を行うように設計できる。 - keyはデータをユニーク
(一意) に識別するためのもので, 型として文字列 (ユーザ名やアイテム名など) だけでなく, 数値列, 文字と数値の組み合わせ (ユーザIDやアイテムID), リストなども使える。 - valueはkeyに関連付けられたデータで,
keyと同じ型を取れ, 複数のIDを組み合わせたリストや構造体も扱える。したがってReducerでの集約を, 数値の集計だけでなく, 結合してリスト作成することもできる。 - Mapperの処理が終わってはじめて,
Reducerの処理が始まる。 - 同じkeyに関連付けられた全データ
(keyが同じkeyとvalueの全ペア), は同じReducerにソートして送られる。 - Reducerの出力はHDFSに書き出され,
これを次のMapperの入力として使える。つまり, MapReduceの処理が何段階に分かれても, データローカリティを確保できる。
MapReduceへマップする
この処理を各段階ごとに見ていきます。
第一段階:履歴からユーザシーケンスへ
前回登場したアイテムシーケンスはアイテム毎にユーザを集約したリスト形式のデータですが,
この段階ではkeyをユーザ,
- ①は履歴の各行から,
ユーザ名をkey, ユーザが評価した映画をvalueとして抽出します。 - ②でユーザ名でMapperの出力結果,
key とvalueのペアをソートします。ユーザ名が同じkeyとvalueのペアは同じReducerに送られます。 - ③は各keyについてvalueを結合することで,
リストを生成します。
次に,
- ユーザ名 → ユーザID
- 映画のタイトル → アイテムID
このデータは各行がタブ区切りになっており,
リスト1 Mapperの例
#!/usr/bin/perl
#Perlスクリプトの安全性を高める。
use strict;
#スクリプト内で利用する変数の宣言
my $mem_id;
my $item_id;
#標準入力からデータファイルを一行毎読みこむ
while (<STDIN>) {
#改行コードの削除
chomp $_;
my @string = split ( /\t/, $_);
$mem_id = $string[0];
if ( $mem_id eq '' ) { next; }
$item_id = $string[1];
if ( $item_id eq '' ) { next; }
#出力形式はkeyを最初に,タブで区切ってvalueを一行毎に出力する。
#タブ以外でも可能であるが,その場合はkeyおよびvalueに含まれないものを使う。
print ($mem_id . "\t" . $item_id . "\n" );
}
リスト2 Reducerの例
#!/usr/bin/perl
use strict;
#スクリプト内で利用する変数の宣言
my ($key,$value);
#key値の比較に用いる変数の初期化。ここではkeyが文字列であるので,空白化している。
my $key_current = "";
my $cnt = 0;
my @list = ();
#標準入力からMapperの出力データを一行毎読みこむ
while (<STDIN>) {
chomp $_;
#入力したデータをkeyとvalueのペアに分割する。分割するデリミタはMap関数の出力形式に合わせる。
#ここでは前出のMap関数がタブ区切りで出力したので,デリミタをタブにする。
($key,$value) = split(/\t/,$_);
#key値が変わった=以前のkeyに関するデータの読み込みが終了のサインなので,
#対応するkey(以前のkey)に対する処理を開始する。
if ( $key ne $key_current ) {
#valueが存在するkeyについてvalueを出力する。
if ( $#list > -1 ) {
print ($key_current . "\t");
foreach ( @list ) {
print ($_ . ":");
}
print ("\n");
}
#比較対象となるkeyを変更する。
$key_current = $key;
@list = ();
}
#keyについてvalueをリスト化する。
push @list,$value;
}
#標準入力を読み終えた段階で,まだ出力していないkeyに対し,集約を行い,その結果を出力する。
print ($key . "\t");
if ( $#list > -1 ) {
foreach ( @list ) {
print ($_ . ":");
}
print ("\n");
}
次回はユーザシーケンスからの説明になります。