Hadoopでレコメンドシステムを作ろう

第8回 コンテンツベースのレコメンドシステムのHadoop実装[後編]

この記事を読むのに必要な時間:およそ 4 分

第一段階

この段階で単語ごとのTFおよびIDFを計算します。

各アイテムで単語が出現する頻度を「各アイテムで単語が出現するレビューの頻度」としているので,Mapperでは前回同様に,レビューごとに <単語 アイテムID> を出力します。

このペアは同一文書から複数回出現しないようにしています。そこで,Reducerを次のように変更します。

リスト1 第一段階でのReducer

#!/usr/bin/perl
use strict;

my ($key,$value);
my $key_current = "";
my $cnt = 0;
my $idf = 0;
my %tf = ();
my $v = 0;

while (<STDIN>) {
        chomp $_;
      #単語を$key 、アイテムを$valueとする
        ($key,$value) = split(/\t/,$_);
        if ( $key ne $key_current )  {
            #単語ごとのユニークアイテム数を取得
                $idf = keys %tf;
                #ここではユニークアイテム数の値が5以上の単語について処理を行っています
                if ( $idf > 5 ) {
                        print ($key_current . "\t");
                        foreach ( keys %tf ) {
                  #単語ごとのTFとIDFの積を計算
                                $v = $tf{$_}*log(100000/$idf)/log(2);
                                print ($_ . "-" . $v . ":");
                        }
                        print ("\n");
                }
                $key_current = $key;
                $idf = 0;
                %tf = ();
        }
      #アイテムごとの出現レビュー数をカウント
        $tf{$value} += 1;
}
@list = keys %tf;
$idf = keys %tf;
if ( $#list > -1 ) {
        print ($key . "\t");
        foreach ( keys %tf ) {
                $v = $tf{$_}*log(100000/$idf);
                print ($_ . "-" . $v . ":");
        }
        print ("\n");
}

各単語で出現したアイテムIDの数は文書の数と等しくなり,これがTFに相当します。また,各単語に集約されたアイテムIDの異なる数が,各単語を含む文書の数になります。ここではあらかじめ全アイテムの数が100000とわかっているものとして,IDFを計算します。

第二段階

ここでは第一段階の出力を使って,アイテムのペア及びTFとIDFの積の値を集約していきます。

Mapperの主な処理は,ターゲットアイテム(計算の基準となるアイテム,全てのアイテムがターゲットアイテムになります)とそれとペアとなるアイテムのペアを単語ごとに,TFとIDFの積と合わせて出力しています。

リスト2 第二段階でのMapper

#!/usr/bin/perl
use strict;

my $i = 0;
my $h_term = 0;
my $h_v = 0;
my $item_id0 = 0;
my $item_id1 = 0;
my $length = 0;
my @list = ();
my @string = ();

while (<STDIN>) {
        chomp $_;
        my @string = split ( /\t/, $_);
      #アイテムリストを取得
        my @list = split (/:/,$string[1]);
        my $length = @list;
        if ( $length > 1 ) {
             #アイテムリストの先頭からアイテムの組み合わせとそのTFとIDFの積をペアとして出力
                for ( $i = 0; $i < $length; $i++ ) {
                        $h_term = shift @list;
                        $h_term =~/(.+)\-(.+)/;
                        $item_id0 = $1;
                        $h_v = $2;
                    #ターゲットアイテムについてアイテムIDとそのTFとIDFの積を出力
                        print ( $item_id0 . "\t" . $h_v . ">");
                    #ターゲットアイテムとペアを組むアイテムIDとそのTFとIDFの積を出力
                        foreach ( @list ) {
                                print ( $_ . ":" );
                        }
                        print ("\n" );
                        push @list,$h_term;
                }
        }
       #ペアが存在しないアイテムIDを出力
        else {
                $list[0] =~/(.+)\-(.+)/;
                $item_id0 = $1;
                $h_v = $2;
                print ( $item_id0 . "\t" . $h_v . ">" . $item_id0 . "\n" );
        }
}

リスト3 第二段階でのReducer

#!/usr/bin/perl
use strict;

my ($key,$value) = ();
my $key_current = "";
my $id;
my $count = 0;
my %co_count = ();
my %flag = ();
my @plist = ();
my @list = ();

while (<STDIN>) {
        chomp $_;
        ($key,$value) = split(/\t/,$_);

        if ( $key ne $key_current )  {
                if ( defined $flag{$key_current} ) {
                        foreach $id ( sort keys (%co_count) ) {
                                print ($key_current . ":" . $id . "\t" . $count . ">" . $co_count{$id} . ">" . 0 . "\n");
                                print ($id . ":" . $key_current . "\t" . 0 . ">" . $co_count{$id} . ">" . $count . "\n");
                        }
                }
                $key_current = $key;
                $count = 0;
                %co_count = %flag = @list = ();
        }
        if ( $value =~ /:/ ) {
                @plist = split(/>/,$value);
                @list = split (/:/,$plist[1]);
                foreach ( @list ) {
                        $_ =~/(.+)\-(.+)/;
                        $id = $1;
                        $co_count{$id} += $2;
                }
                $count += $plist[0];
                $flag{$key} = 1;
        }
        else {
                $value =~ /(.+)>.+/;
                $count += $1;
         }
}
if ( defined $flag{$key_current} ) {
        foreach $id ( sort keys (%co_count) ) {
                print ($key_current . ":" . $id . "\t" . $count . ">" . $co_count{$id} . ">" . 0 . "\n");
                print ($id . ":" . $key_current . "\t" . 0 . ">" . $co_count{$id} . ">" .
$count . "\n");
        }
}

第三段階

ここでの処理は第5回で利用したReducerのうち

$corr = $co/($key_count{$key_current}*$key_count{$id});

の箇所を

$corr = $co/sqrt($key_count{$key_current}*$key_count{$id});

とします。

また,今回はvalueが小数になるので次のような変更も必要です。

if ( $value =~ /(\d+)>(\d+)>(\d+)/ ) {

if ( $value =~ /(.+)>(.+)>(.+)/ ) {

Mapperはそのまま使えます。

TF/IDFの正確な値でなく,TF/IDFに基づく相対的な大小関係を求めたいのであれば,第5回で紹介したように第二段階で終了することができます。単語の重み付けにはTF/IDF以外にも多くの方法がありますし,単語の品詞情報や共起情報あるいはN-gramの利用もあります。

次回は最終回で,レコメンド全般について振り返ります。

著者プロフィール

川前徳章(かわまえのりあき)

工学博士,NTTコムウェア 研究開発部 勤務。専門は情報検索,統計的機械学習,マーケティングサイエンス。現在はレコメンドシステムと感性検索の研究と開発に従事。

東京電機大学安田研究室 研究員