Stellaqua - TOMの技術日記 このページをアンテナに追加 RSSフィード

2010年08月08日

[]Amazon EC2上のHadoopMeCabを使えるようにしてみた…い

最近すっかり"Webサービスを作ってみた"系の記事ばっかりでしたが、今回は久々にEC2上でのHadoopのお話。

以前の記事EC2上でHadoopを動かした時は、MeCabデフォルトで入っていなかったので、事前に自宅サーバ上で分かち書きしたデータを使っていました。

ただ、日本語処理するならMeCabはほぼ必須だろうし、せっかくならMeCabが使える状態でMapReduceしたいのが人情ってもんです。

という訳で、EC2上のHadoopを使って、MeCabを利用した日本語文章の単語カウントMapReduceに挑戦してみました。

基本的な方向性としては以下の通りです。

  1. EC2上でインスタンス起動。
  2. 起動したインスタンスMeCabインストール
  3. AMIとして保存。
  4. Hadoopを起動する時にこのAMIが使われるようにする。

で、実際にやってみた訳なんですが、結論から言うとうまくいきませんでした…。

AMIを保存して、そのAMIが使われるようにするところまではできたんですが、そのインスタンスログインできない状態になってしまって、実際にMeCabを利用したMapReduceを動かすところまでできていません。

という事で途中までではありますが、やってみた手順を書き残しておきたいと思います。

以下、"[local]$"がローカルのサーバ上、"[ec2]$"がEC2上でのプロンプトを表す事とします。

EC2上でHadoopを起動してログインする

まずはEC2上にインスタンスを1つ起動させてログインします。

[local]$ hadoop-ec2 launch-master hadoop-test 1
[local]$ scp -i stellaqua.id *.pem root@ec2-XXX-XXX-XXX-XXX.compute-1.amazonaws.com:/mnt
[local]$ hadoop-ec2 login hadoop-test

タイムゾーンを変更する

今回(というか毎度)参考にさせて頂いているid:rx7さんの記事によるとタイムゾーンの設定をしておいた方がよいとの事なので、ついでにここでタイムゾーンの変更をしておきます。

[ec2]$ cp /usr/share/zoneinfo/Japan /etc/localtime
cp: `/etc/localtime' を上書きしてもよろしいですか(yes/no)? y

MeCabインストールする

続いて本命のMeCabインストールします。

[ec2]$ yum -y update
[ec2]$ yum -y install mecab mecab-devel mecab-ipadic
[ec2]$ echo 'こんにちは' | mecab
こんにちは      感動詞,*,*,*,*,*,こんにちは,コンニチハ,コンニチワ
EOS

ちゃんとMeCabが使えるようになりましたね!

EC2root権限が与えられるので、yumで必要なものをガシガシ入れてしまうとよいですね。

AMIを作成してS3にアップロードする

AMI作成の手順については、毎度お世話になっているid:rx7さんの記事を参考にさせて頂きました。

Amazon EC2/S3を使ってみた - 3.EC2起動後〜AMI作成 - 元RX-7乗りの適当な日々

[ec2]$ cd /mnt/
[ec2]$ ec2-bundle-vol -d /mnt --privatekey pk-XXXX.pem --cert cert-XXXX.pem --user XXXX-XXXX-XXXX -p hadoop-0.17.0-i386 -r i386
[ec2]$ ec2-upload-bundle --bucket stellaqua/ec2_images/hadoop_mecab --manifest hadoop-0.17.0-i386.manifest.xml --access-key XXXX --secret-key XXXX
[ec2]$ exit
[local]$ hadoop-ec2 terminate-cluster hadoop-test
[local]$ ec2-register stellaqua/ec2_images/hadoop_mecab/hadoop-0.17.0-i386.manifest.xml
IMAGE   ami-XXXX
[local]$ vi hadoop-ec2-env.sh
→"S3_BUCKET=hadoop-ec2-images"となっているところを、上記で指定したバケットに変える

ポイントは、manifestファイルをHadoopのバージョンとアーキテクチャの名前を付けて保存する事と、設定ファイルでAMIを保存したバケットを設定しておく事ですね。

これで、hadoop-ec2コマンドでHadoopを起動する時に、登録したAMIが使われるようになるはずです。

早速、クラスタを起動してログインしてみます。

[local]$ hadoop-ec2 launch-cluster hadoop-test 1
[local]$ hadoop-ec2 login hadoop-test

これでログインできるはずなんですが、なぜかログインしに行ったままウンともスンとも言わない状態になってしまいます。

"Permission denied"とも言われないし、netstatで見るとESTABLISHEDにはなっているので、接続自体はできているとは思うのですが…。その後も色々試してはみたんですが、現時点でお手上げ状態になっています…。

また折を見て試してみて、うまくいったら記事にしようかなと思います。

2010年04月04日

[][]Amazon Elastic MapReduceHadoop Streamingする時にライブラリをrequireする方法

ちょっと元データの件数が大量にある処理をしたいという要件があって、普通に逐次処理していくと恐ろしく時間が掛かるので、「こんな時こそHadoop!」って事で、久々にHadoopをいじくっていました。

ただ、自宅サーバでやろうとすると、いくら分散処理できるとは言っても結局処理するのは物理的には1台な訳で、メモリを使い切ってスワップしまくってウンともスンとも言わなくなってしまうという、とっても悲しい状況になってしまいました。

「そんな時はAmazon先生にお願い!」って事で、Amazon Elastic MapReduceを試していたんですが、ちょっとハマってしまったところがあったので、備忘録がてら記事にしようかと思います。

Mapper/Reducer以外のファイルが使えない!

以前にAmazon Elastic MapReduceを試した時は、MapperとReducerが1ファイルだけの簡単な処理でやってみただけでした。

ただ、今回はもうちょっと処理が複雑で、Mapperから別ファイルをrequireしたり、serializeしてファイルに書き出しておいたデータを使ったりするものでした。

CLIでAmazon Elastic MapReduceを実行する場合、一番簡単な書き方で以下のような形になります。

$ elastic-mapreduce --create --stream \
--input s3n://stellaqua/mapreduce/inputs \
--output s3n://stellaqua/mapreduce/outputs \
--mapper s3n://stellaqua/mapreduce/map.php \
--reducer s3n://stellaqua/mapreduce/reduce.php

Mapper/ReducerはS3に置いたものを指定する事ができて、初期化時に勝手にS3から読み込んできてHadoopの実行ノードに転送して実行してくれます。

しかし、Mapper/Reducer以外のファイルは例えS3上に置いておいても、勝手に読み込んだりはしれくれないので、Mapperとかでrequireしようとしても、読み込む事ができません。

で、どうすればいいか途方に暮れつつ調べていたら、以下のサイトさんで解決方法を見い出す事ができました。

Soffritto::Journal

"--cache-archive"というオプションを使う事で、Hadoop初期化時に各ノードに転送する事ができるようです。

まずMapper/Reducer以外の、ライブラリなどのファイルを一つのディレクトリにまとめておいて、jarコマンドでアーカイブします。

$ jar cvf lib.jar -C lib/ .

Mapper/Reducerでrequireしたりする時は、相対パスで書いておきます。

<?php
require 'lib/Hogeclass.php';
?>

後はElastic MapReduceを実行する時に、"--cache-archive"オプションを使ってjarファイルを指定してやればオッケーです。

$ elastic-mapreduce --create --stream \
--input s3n://stellaqua/inputs/input.dat \
--output s3n://stellaqua/outputs \
--mapper s3n://stellaqua/map.php \
--reducer s3n://stellaqua/reduce.php \
--cache-archive s3n://stellaqua/lib.jar#lib \
--num-instances 4 \
--log-uri s3n://stellaqua/logs

ついでながら、上記では、起動インスタンス数とログ出力先の指定もしています。

うまく使えば、ローカルにPEARを展開したものをアーカイブして、Hadoop Streamingから利用するとかできそうですね。

という訳で実際にElastic MapReduce上で処理させてみたんですが、またしても問題が…。

処理が成功しようが失敗しようが料金が掛かる
1回分の料金が安いとは言え、何度も失敗しているとその分料金がかさんでいくのは微妙に痛いです…。
どこまで処理が進んだか分かりづらい
一応、AWS Management Console上にデバッグコンソールというのもあってsyslogが見れるんですが、ちゃんと処理が進んでいるのかどうか微妙に分かりづらいです…。

…という事で、何回もの失敗を乗り越えてやっとまともに動くようになったので、1時間ほど動かしてみたんですが、ちゃんと処理が進んでいるのかどうかよく分からなかったので止めてしまいました。Elastic MapReduceを使いこなすには、もうちょっと研究が必要そうですね…。

2009年03月27日

[][]Amazon EC2上でHadoop Streamingによる分散処理をPHPでやってみた

あいかわらずHadoopStreamingが楽しくてやっているんですが、そろそろ自宅サーバ1台だけで処理するのは限界っぽいので、AmazonEC2上でHadoopStreamingにチャレンジしてみました。

AmazonEC2の導入に関しては、以下のまとめ記事からのリンクを参照しまくりさせてもらいました。多謝。m(_ _)m

Amazon EC2/S3を使ってみた - まとめ (Amazon Web Services関連エントリ目次) - 元RX-7乗りの適当な日々

そして、AmazonEC2上でのHadoopStreamingの動かし方に関しては、以下の記事を参考にさせてもらいました。

hadoop-ec2でアクセス解析してみたよ! - soffritto::journal

階層的クラスタリングをEC2上で動かしてみる

とりあえずテストという事で、前回の記事で作成した階層的クラスタリングのMapReduceを、EC2上で動かしてみようと思います。

EC2のセットアップに関しては上で紹介した記事が非常によくまとまっているので、ここでは手順等は省略。

今回はEC2への接続周りはセットアップ済みという前提で、自宅のサーバ上からどうやってEC2上のHadoopを動かすかという辺りの手順を書いてみたいと思います。ただ、やっている事は上で紹介したもう一つの記事とほとんど同じなので、手順もほとんど同じになっています。

Hadoop用のマシンイメージにPHPが含まれているか心配だったんですが、ちゃんと入っていました。*1

以下、"[local]$"のプロンプトは自宅サーバ上、"[EC2]$"のプロンプトはEC2上でのコマンドを表します。

EC2上でHadoopを起動する

[local]$ hadoop-ec2 launch-cluster hadoop-test 2

これだけです…。たったこれだけで、EC2上に仮想のマシンが用意されて、Hadoopが使えるようになります。素晴らしい〜。

EC2上に必要なファイルをアップロードする

[local]$ hadoop-ec2 push hadoop-test reduce.php
[local]$ hadoop-ec2 push hadoop-test reduce2.php
[local]$ hadoop-ec2 push hadoop-test Reducer.php
[local]$ hadoop-ec2 push hadoop-test Iterator.php
[local]$ hadoop-ec2 push hadoop-test words.txt
[local]$ hadoop-ec2 push hadoop-test hcluster.sh

前回はMapperで形態素解析して入力データを作っていたんですが、残念ながらEC2上のマシンイメージにMeCabが入ってなかったので、今回は事前にMapper処理済みのデータを用意する事にしました。

あと、拙作のPHP-Hadoop-Streaming-Frontendディレクトリ階層を付けずにフラットな状態でアップロードするようにしました。ファイルアップした後にディレクトリ階層化してもいいんですが、面倒なので…。

れっつ HadoopStreaming on EC2!

これで準備OK! 早速動かしてみましょう。

[local]$ hadoop-ec2 login hadoop-test
[EC2]$ mkdir inputs
[EC2]$ mkdir outputs
[EC2]$ cp -i words.txt inputs
[EC2]$ ./hcluster.sh

非常にサクサク動きますねぇ。処理時間はだいたい30分ぐらいでした。今回のお題はデータ量も計算量もそんなに大きくないので、自宅サーバでやった場合とあんまり違いは出なかったですね。(^^;ゞ

EC2上のインスタンスを停止する

最後に忘れずにEC2上のインスタンスを停止しておきます。これを忘れると、延々と課金され続けてしまうので注意…。

[EC2]$ exit
[local]$ hadoop-ec2 terminate-cluster hadoop-test

感想

実はやってみるまでは「EC2って面白そうだけどお金も掛かるし、何か設定とか面倒臭そうだよなぁ…。」と思っていたんですが、正直なところそこまで面倒でもないし、何より安い!

1台/h=0.10$≒9.88337616円(執筆時点)なので、1台を24時間ずっと使っていたとしても1日240円もしないという…。今、利用状況を見てみたら、何だかんだで色々遊ぶのに使って6h…その他、データ転送代などがちょこちょこ入って、占めて0.65$=63.8695097円也…。

膨大な量のデータを処理したい時は、事前にMapReduceを作っておいて、EC2上で計算させる…っていう使い方をすれば、そんなにコストも掛からないし、かなり使えそうです。

問題は、いかにデータ処理の計算をMapReduceできる形に落とすか…ですね…。この辺り、自分自身は専門分野ではないし、ネット上でもなかなか情報がなくてつらいところなんですが…やっていて楽しいところではあるので、集合知プログラミングシリーズの記事の中で、また少しずつやっていこうと思っています。

*1:バージョンは確認し忘れた…。

2009年03月17日

[][]何番煎じか分からないけど集合知プログラミングをPHPでやってみた その7「階層的クラスタリングによりグループを見つけ出す」

前回は話だけで終わってしまったので、今回はソースコード中心です。

アイテム同士の距離の計算に必要な情報を出力するReducerを実装する

という訳で早速ですが、前回延々と話をしていた事をReducerに実装します。

#!/usr/bin/php
<?php
require_once(dirname(dirname(__FILE__)).'/lib/HadoopStreaming/Reducer.php');

class Reducer extends HadoopStreaming_Reducer
{
    public function reduce ( $key, $values )
    {
        $wordcount = array();
        while ( $values->has_next_value ) {
            list($id, $count) = explode(':', $values->current_value);
            $wordcount[$id]++;
            $entrycount[$id] = $count > 0 ? $count : 1;
            $values->next();
        }
        if ( count($wordcount) >= 2 ) {
            $keys = array_keys($wordcount);
            for ( $id1=0; $id1<count($keys)-1; $id1++ ) {
                for ( $id2=$id1+1; $id2<count($keys); $id2++ ) {
                    $emitkey = $keys[$id1].':'.$keys[$id2];
                    $wc1 = $wordcount[$keys[$id1]] / $entrycount[$keys[$id1]];
                    $wc2 = $wordcount[$keys[$id2]] / $entrycount[$keys[$id2]];
                    $sum1 = $wc1;
                    $sum2 = $wc2;
                    $pow1 = pow($wc1, 2);
                    $pow2 = pow($wc2, 2);
                    $mul = $wc1 * $wc2;
                    $this->emit($emitkey, "sum1:${sum1}");
                    $this->emit($emitkey, "sum2:${sum2}");
                    $this->emit($emitkey, "pow1:${pow1}");
                    $this->emit($emitkey, "pow2:${pow2}");
                    $this->emit($emitkey, "mul:${mul}");
                    $this->emit($emitkey, "count:1");
                }
            }
        }
    }
}

$reducer = new Reducer();
$reducer->run();
?>

Reducerには、"単語=>[エントリID,エントリID,…]"という形でデータが渡ってくるので、一旦、$wordcountという配列でエントリID毎に出現単語数を集計しています。

これで$wordcountのキーには、この単語が現れるエントリのIDが並ぶので、このエントリID同士の総当りをループさせて、必要な情報を計算していきます。

なお、"クラスタ化した時は各アイテムの中間の位置を新しいクラスタの位置にする"という処理を実現する為に、各クラスタに含まれるエントリの数が入力データに含まれるようにしていて、各エントリの単語数($wc1,$wc2)は各クラスタ内のエントリ数($entrycount)で割った値になるようにしています。

計算した値は、後でもう1回Reducerを掛けて集計するので、ここではkeyを付けてどんどんemitしていくだけにします。

このReducerを掛けると、以下のように、2つのエントリ同士の計算に必要な情報がバラバラと出力されます。

:
0:13	count:1
0:13	count:1
0:13	mul:1
0:13	mul:10
0:13	pow1:1
0:13	pow1:4
0:13	pow2:1
0:13	pow2:25
0:13	sum1:1
0:13	sum1:2
0:13	sum2:1
0:13	sum2:5
:

アイテム同士の距離を計算して出力するReducerを実装する

続いて、先ほど得られた出力を使って、各アイテム同士の距離を計算して出力するReducerを実装します。

#!/usr/bin/php
<?php
require_once(dirname(dirname(__FILE__)).'/lib/HadoopStreaming/Reducer.php');

class Reducer extends HadoopStreaming_Reducer
{
    public function reduce ( $key, $values )
    {
        $wc = array();
        while ( $values->has_next_value ) {
            list($target, $value) = explode(':', $values->current_value);
            $wc[$target] += $value;
            $values->next();
        }
        $num = $wc['mul'] - ( $wc['sum1'] * $wc['sum2'] / $wc['count'] );
        $den = sqrt( ($wc['pow1'] - pow($wc['sum1'], 2) / $wc['count'])
                    * ($wc['pow2'] - pow($wc['sum2'], 2) / $wc['count']) );
        $result = ( $den === 0.0 ) ? 0.0 : ( 1.0 - $num / $den );
        $this->emit($key, $result);
    }
}

$reducer = new Reducer();
$reducer->run();
?>

これで、はてブのHotEntryから取得したデータを使って、どのエントリ同士が似ているかを計算する事ができるようになりました。

似たアイテム同士をクラスタにして後は同じ事の繰返し

ここまで来たら次は、一番距離が近かったエントリ同士を1つのクラスタとしてくっつけて、後はクラスタが1つになるまで繰返しMapReduceを掛けていけば、階層的クラスタリングの完成です。

…となるんですが、HadoopStreamingを使って複数のMapper/Reducerを組み合わせて処理させる方法が分からなかったので、仕方なくシェルスクリプトで適宜中間ファイルを処理しながら、MapReduceを繰り返して実行するようにしました。

という事で、かなり無理矢理感はあるんですが…結果的に、全体の処理をループする部分は、次のようなスクリプトになりました。

#!/bin/sh
dfs='/home/hadoop/hadoop-0.18.3/bin/hadoop dfs'
hadoop='/home/hadoop/hadoop-0.18.3/bin/hadoop jar /home/hadoop/hadoop-0.18.3/contrib/streaming/hadoop-0.18.3-streaming.jar'

$dfs -rmr outputs
$dfs -rm inputs/hotentry_descriptions.txt
$dfs -put ~/inputs/hotentry_descriptions.txt inputs
$hadoop -input inputs/hotentry_descriptions.txt -output outputs -mapper ~/bin/mapreduce/php/hcluster/map.php
rm -f ~/outputs/part-*
$dfs -get outputs/part-* ~/outputs
cat ~/outputs/part-* >~/inputs/words.txt
$dfs -rm inputs/words.txt
$dfs -put ~/inputs/words.txt inputs/words.txt

cid=1
>~/outputs/clusters.txt
cc=`cut -f 2 ~/inputs/words.txt | cut -d ':' -f 1 | sort | uniq | wc -l`

while [ $cc -gt 1 ];
do
    $dfs -rmr outputs
    $hadoop -input inputs/words.txt -output outputs -mapper cat -reducer ~/bin/mapreduce/php/hcluster/reduce.php
    $dfs -rm inputs/part-*
    $dfs -mv outputs/part-* inputs
    $dfs -rmr outputs
    $hadoop -input inputs/part-* -output outputs -mapper cat -reducer ~/bin/mapreduce/php/hcluster/reduce2.php

    closest=`$dfs -cat outputs/part-* | sort -nk 2 | head -1 | cut -f 1`
    id1=`echo $closest | cut -d ':' -f 1`
    id2=`echo $closest | cut -d ':' -f 2`
    if [ $id1 -lt 0 ]; then
        c1=`cut -f 2 ~/inputs/words.txt|grep '\'$id1|head -1|cut -d ':' -f 2`
    else
        c1=1
    fi
    if [ $id2 -lt 0 ]; then
        c2=`cut -f 2 ~/inputs/words.txt|grep '\'$id2|head -1|cut -d ':' -f 2`
    else
        c2=1
    fi
    ec=`expr $c1 + $c2`
    echo "-$cid     $closest">>~/outputs/clusters.txt
    sed 's/\t'$id1'\(:.*$\|$\)/\t-'$cid':'$ec'/' ~/inputs/words.txt | sed 's/\t'$id2'\(:.*$\|$\)/\t-'$cid':'$ec'/' >~/inputs/words.txt_tmp
    mv -f ~/inputs/words.txt_tmp ~/inputs/words.txt
    $dfs -rm inputs/words.txt
    $dfs -put ~/inputs/words.txt inputs/words.txt
    cid=`expr $cid + 1`
    cc=`cut -f 2 ~/inputs/words.txt | cut -d ':' -f 1 | sort | uniq | wc -l`
    echo 'クラスタ数:'$cc
done

最初に1回Mapperを使って単語リストを作成した後に、クラスタの数が1つになるまでループしながら繰返し2種類のReducerを実行するようにしています。

結果発表

実行結果はテキストとして出力しているので、このままだとどんな結果なのかよく分かりません。

元書では、ノードをピラミッドの形で並べて表示する"デンドログラム"というグラフの形式で結果を確認するようになっていて、グラフィックとして出力するプログラムも作っているんですが、そんなのまで作るのは面倒なので、今回はGraphvizを使ってグラフを生成するようにしました。

得られた結果をdot形式に直して、最初に取得したタイトルを埋め込んで、デンドログラムっぽい形で出力させてみました。

f:id:stellaqua:20090317220147p:image:w450

今回は、HotEntryの最初のページ(30件)が対象で、なおかつRSSの概要から単語を拾っているだけなので、正直なところあんまりそれっぽい結果とは言えないかもしれないですね。

まぁ、今回の一番の目的は、"いかにMapReduce的に実装するか"だったので、自分的にその目的は果たす事ができたのでよしとしたいと思います。

次回は?

次回は、別のアルゴリズム(K平均法)によるクラスタリングを、またMapReduceを使って挑戦していきたいと思います。

2009年03月11日

[][]何番煎じか分からないけど集合知プログラミングをPHPでやってみた その5「グループを見つけ出す…の下準備…の下準備」

今回から、元書の第3章に入っていきます。第3章は"グループを見つけ出す"というタイトルになっていて、アイテムを複数のグループに分類するアルゴリズムがいくつか紹介されています。今回は、実際にグルーピングをする前段階として、フィード中の単語を数える部分を作ってみます。

…の前にphp_hadoop_streaming_frontendの修正について

以前の記事の中で、php_hadoop_streaming_frontendというのを作成して、ちょうどサンプルで単語を数えるというのをやった訳ですが、そのまま応用できそうなので、今回はそれを流用します。が、そのまま使おうとするとちょっと問題があって、少しばかり手を加えました。

Mapperの中で定義するmap関数の引数が($key,$value)となっているんですが、実は若干手を抜いていて、$valueにSTDINから取得された1行分のデータが入って、$keyには必ずnullが入るという実装にしていました。

と言うのも、Mapperに入ってくる情報は飽くまで1行ずつのデータな行指向の処理になるので、元々、$keyと$valueのセットとして値が入ってくる訳じゃないんですよね。例えば、ログファイルなんかを処理する時には、行の羅列に対して1行ずつ処理していけばいいだけなので、Mapperに渡すデータは特にkeyとvalueの組み合わせというのは必要ありません。

ただ今回は、最終的に"どの単語がどのエントリの中に含まれている"という情報が欲しいので、"key=>エントリID,value=>エントリ内容"というデータが必要になります。そうするとmapメソッドの引数の$keyが常にnullでは困るので、Mapperに渡すデータを、keyとvalueのタブ区切りというフォーマットに対応できるようにしました。

最終的にReducerが吐き出すデータもタブ区切りになっているので、Mapperがタブ区切りに対応していると、多段階のMapReduceをやりたい場合にも都合が良さそうですね。( ̄ー ̄)

ちなみに、GitHubに上げたソースコードは修正済みです。→http://github.com/stellaqua/php-hadoop-streaming-frontend/tree

元データを準備する

という事でやっと本題です。今回は最初から元書をガン無視して、はてブのHotEntryを元データのターゲットにしたいと思います。*1

まずは、はてブのHotEntryのフィードから、エントリのタイトルと概要を取得してきてデータを作成します。

<?php
require_once('XML/RSS.php');
$rss = new XML_RSS('http://b.hatena.ne.jp/hotentry.rss');
$rss->parse();
$items = $rss->getItems();
$titles = '';
$descriptions = '';
for ( $id=0; $id<count($items); $id++ ) {
    $title = preg_replace('/[\t\r\n]/', '', $items[$id]['title']);
    $description = preg_replace('/[\t\r\n]/', '', $items[$id]['description']);
    $titles .= $id."\t".$title."\n";
    $descriptions .= $id."\t".$description."\n";
}
file_put_contents(dirname(__FILE__).'/hotentry_titles.txt', $titles);
file_put_contents(dirname(__FILE__).'/hotentry_descriptions.txt', $descriptions);
?>

とりあえずデータが作れればいいので、こんなところで…。タイトルと概要でファイルを分けているのは、後でMapReduceする時に、タイトルがkeyやvalueの中に含まれると長ったらしくて困るので、連番を振ってIDで管理する為です。

単語の出現頻度のデータを作る

元書だと、データ形式に関して、"列方向に単語、行方向にブログ"という超巨大テーブルを用意して、どのブログにどの単語がいくつあるかを全て埋めていくという、なかなか恐ろしい事をやっているんですが(^^;、「さすがにそれは無いだろ。」という事で、我が道を行く事にします。

せっかくMapReduceができるようになったので、元書のベタ移植ではなく、アルゴリズムは借用させてもらいつつも、"いかにMapReduceに載せていけるか"という事に挑戦していこうと思っています。

という事で、まずはMapperから。

#!/usr/bin/php
<?php
require_once(dirname(dirname(__FILE__)).'/lib/HadoopStreaming/Mapper.php');

class Mapper extends HadoopStreaming_Mapper
{
    public function map ( $key, $value )
    {
        $value = escapeshellcmd($value);
        $result = shell_exec("echo ${value}|/usr/bin/mecab");
        $words = explode(PHP_EOL, $result, -1);
        foreach ( $words as $word ) {
            if ( $word === 'EOS' ) {
                break;
            }
            $wordinfo = split("\t|,", $word);
            if ( $wordinfo[1] === '名詞' || $wordinfo[1] === '動詞' ) {
                $wordinfo[7] = ( $wordinfo[7] !== '*' ) ? $wordinfo[7] : $wordinfo[0];
                $this->emit($wordinfo[7], $key);
            }
        }
    }
}

$mapper = new Mapper( $is_tab_separated = true );
$mapper->run();
?>

記事の冒頭で書いたフレームワークの修正で、Mapperのコンストラクタで入力がタブ区切りかどうか指定できるようになりました。(デフォルトはfalseです。) 今回はエントリIDをキーに持つデータが入力値なので、trueに設定しています。

あと、HadoopStreamingについて書いた記事では、元から分かち書きしたデータを使って単純にスペースで分割しましたが、今回はせっかくなので単語の品詞にも注目して、名詞と動詞のみ抜き出すようにしてみました。*2

これで、"単語=>[エントリID,エントリID,…]"という、各単語毎にその単語が含まれるエントリのIDが列挙される形でMapperから出力されます。

では続いてReducerに…といきたいところですが、だいぶ長くなってしまったので、続きは次回に…。

*1:元書は、筆者が用意したRSSフィードURLの一覧を使って、フィードから単語数をカウントするようになっていました。

*2:本当は名付けて.ね〜むでやっているように、"○○する"を一語とみなすとかもした方がいいんですが、今回は手を抜いて省略…。