jubatus-exampleを全部動かしてみる（前編）

この記事は [Jubatus Advent Calendar](http://qiita.com/advent-calendar/2016/jubatus) の2日目の記事です。
Jubatus advent calendar 2日目からはJubatusに搭載されているアルゴリズムの紹介も兼ねて、jubatus-exampleを順番に全て実行していきます。

# jubatus-exampleとは

githubの[こちら](https://github.com/jubatus/jubatus-example)で公開されている、Jubatusを使ったサンプルプログラム集です。
READMEに書いてある通り、現在8種の機械学習エンジンについて、17種のexampleが公開されています。
エンジンを動かすだけの単純なものから、少々複雑なデータ分析を行うものまで様々です。
クライアント言語もC++/Python/Ruby/Javaの4種の言語で実装されたものがあります。
\# 歯抜けになっているものも多いので、コントリビューションお待ちしてます！
~~今回は唯一全てのexampleが実装されているPythonのexampleを実行していきます。~~
後編で扱うmalware classificationはc++だけ、winequalityはrubyだけでした。ごめんなさい。

# 前編で実行するexample

前編の今日は単純なexampleを中心に実行していきます。(括弧内は使用する機械学習エンジン)
- shogun (classifier)
- gender (classifier)
- movielens (recommender)
- npb_similar_player (recommender)


# 環境
今回のexampleの実行は全て以下のような環境で行います。

|項目            | バージョンなど             |
|----------------|--------------------------|
|OS              | ubuntu 14.04             |
|Jubatus version | 1.0.0 (aptでインストール) |
|Python          | 3.5.2                    | 




# 準備
## jubatusのインストールと環境変数の設定
今回はjubatusをaptで入れるのでリポジトリの登録, apt-get, 環境変数の設定をします。

```
$ echo "deb http://download.jubat.us/apt/ubuntu/trusty binary/" | tee /etc/apt/sources.list.d/jubatus.list
$ apt-get update
$ apt-get install jubatus
$ source /opt/jubatus/profile
``` 

## jubatus python クライアントのインストール
pip でjubatusのクライアントをインストールします

```
pip install jubatus
```


## jubatus-exampleのダウンロード
jubatus-exampleのリポジトリをcloneしてきます。

```
git clone http://github.com/jubatus/jubatus-example.git
```

以上で準備は終わりです。

# shogun
最初に動かすのはclassifierのexample "shogun" です。
このexampleでは歴代将軍の名前をjubatusに学習させ、新たに入力された名前がどの将軍家っぽいかを分類します。

## とりあえず動かしてみる

### jubatus サーバの起動
はじめにjubatusサーバを起動させます。サーバの起動には使用するエンジンに合わせた`juba{hoge}` コマンドを実行します。分類器の場合は`jubaclassifier`コマンドです。この時、サーバの設定ファイルの指定が必須となります。今回はjubatus-example/shogun の下にあるshogun.jsonを使います。

```
$ jubaclassifier -f shogun.json
2016-11-30 19:50:32,691 21212 INFO  [server_util.cpp:429] starting jubaclassifier 1.0.0 RPC server at 10.0.2.15:9199                                                                                                                   
    pid                  : 21212
    user                 : TkrUdagawa
    mode                 : standalone mode
    timeout              : 10
    thread               : 2
    datadir              : /tmp
    logdir               : 
    log config           : 
    zookeeper            : 
    name                 : 
    interval sec         : 16
    interval count       : 512
    zookeeper timeout    : 10
    interconnect timeout : 10

2016-11-30 19:50:32,691 21212 INFO  [server_util.cpp:165] load config from local file: /home/TkrUdagawa/jubatus-example/shogun/shogun.json
2016-11-30 19:50:32,692 21212 INFO  [classifier_serv.cpp:115] config loaded: {
  "method": "AROW",
  "converter": {
    "num_filter_types": {},
    "num_filter_rules": [],
    "string_filter_types": {},
    "string_filter_rules": [],
    "num_types": {},
    "num_rules": [],
    "string_types": {
      "unigram": { "method": "ngram", "char_num": "1" }
    },
    "string_rules": [
      { "key": "*", "type": "unigram", "sample_weight": "bin", "global_weight": "bin" }
    ]
  },
  "parameter": {
    "regularization_weight" : 1.0
  }
}

2016-11-30 19:50:32,692 21212 INFO  [server_helper.hpp:226] start listening at port 9199
2016-11-30 19:50:32,692 21212 INFO  [server_helper.hpp:233] jubaclassifier RPC server startup

```

正しくjubatusサーバが起動していれば上記のようなログが出力され、jubatusサーバが9199ポートでリクエストを受け付け始めます。
configの意味は公式ドキュメントの[データ変換](http://jubat.us/ja/fv_convert.html)や[クライアントAPI](http://jubat.us/ja/api/api_classifier.html#configuration)などを参照ください。ざっくりとした解説をすると、classifierのアルゴリズムに`AROW`を指定、文字列の特徴が与えられたら1文字1文字単位で学習する、という設定を与えています。

### クライアントプログラムの実行
jubatus-example/shogun/python の下にあるshogun.pyを実行します。

```
$ python shogun.py
徳川 慶喜
足利 義昭
北条 守時
```

無事に名字と名前の対応がとれた結果が出力されました。

### ちょっとした解説
このexampleでは
1. 将軍の名字をラベル、名前をデータとしてjubatusに学習
2. 名前だけをjubatusに与えて分類

ということをしています。
1.の処理がshogun.pyのtrainメソッド、2.の処理がshogun.pyのpredictメソッドで行われます。

```
1. 学習
def train(client):
    # prepare training data
    # predict the last ones (that are commented out)
    train_data = [  # 学習に用いるデータを(ラベル、Datum)のタプルのリストで作る
        ('徳川', Datum({'name': '家康'})),
        ('徳川', Datum({'name': '秀忠'})),
        ....(中略)       
        ('北条', Datum({'name': '高時'})),
        ('北条', Datum({'name': '貞顕'})),
        # (u'北条', Datum({'name': u'守時'})),  
    ]
    random.shuffle(train_data)
    client.train(train_data)  # jubatusに学習させる
```

ここで出てくるDatum(でーたむ)というデータ型がJubatusで使われる特有のデータ構造になります。
Datumはkey-value型の構造をもっており、数値特徴、文字列特徴、バイナリ特徴の3種類のデータを格納することができます。
この例では `{'name':'家康'}` のように文字列特徴を1つしか渡していませんが、`{'name':'家康', 'stroke': 21}`などのように数値がvalueとなるものを渡すと数値特徴量を含むDatumをつくることができます。

ここで作成したDatumとラベルのタプルのリストを`train`メソッドに渡すことで学習ができます。

```
2. 分類
def predict(client):
    # predict the last shogun
    data = [ # 分析対象のデータをDatumのリストで作る
        Datum({'name': '慶喜'}),
        Datum({'name': '義昭'}),
        Datum({'name': '守時'}),
    ]
    for d in data:
        res = client.classify([d]) # jubatusで分析
        # get the predicted shogun name
        shogun_name = max(res[0], key = lambda x: x.score).label # 分析結果のうち最もスコアの高いラベルを取得
        first_name = d.string_values[0][1]
        _output('{0} {1}\n'.format(shogun_name, first_name))
```

分類するときはDatumのリストを作り、`classify`メソッドを呼び出します。返り値はclassifierが覚えている全てのラベルについてのスコアになります。
したがってクライアント側で、最も高いスコアのラベルを抜き出す処理を入れています。

# gender
次に実行するのは同じくclassifierのexample "gender" です。
このexampleではmale, femaleというラベルと、髪の長さ`hair`、 上半身の服装`top`、下半身の服装`bottom`、身長`height`といった特徴を学習させ、男性を女性の特徴をjubatusに分類させます。

## とりあえず動かしてみる
### jubatusサーバの起動
shogunと同様、まずはjubatusサーバを起動します。今回も分類器なので`jubaclassifier`コマンドを実行します。

```
$ jubaclassifier -f gender.json &
```

### クライアントプログラムの実行

続いてjubatus-example/gender/python にある、クライアントプログラムgender.pyを実行します。

```
$ python gender.py
female: 0.061350
male: 0.367799
------
female: 0.600048
male: -0.372206
------
```

ちゃんと動作すれば男性(male)、女性(female)それぞれのラベルのスコアが出力されます。

### ちょっとした解説

このexampleではjubatusに以下のようなデータを学習させています。

```
train_data = [
    ('male',   Datum({'hair': 'short', 'top': 'sweater', 'bottom': 'jeans', 'height': 1.70})),
    ('female', Datum({'hair': 'long',  'top': 'shirt',   'bottom': 'skirt', 'height': 1.56})),
    ('male',   Datum({'hair': 'short', 'top': 'jacket',  'bottom': 'chino', 'height': 1.65})),
    ('female', Datum({'hair': 'short', 'top': 'T shirt', 'bottom': 'jeans', 'height': 1.72})),
    ('male',   Datum({'hair': 'long',  'top': 'T shirt', 'bottom': 'jeans', 'height': 1.82})),
    ('female', Datum({'hair': 'long',  'top': 'jacket',  'bottom': 'skirt', 'height': 1.43})),
#    ('male',   Datum({'hair': 'short', 'top': 'jacket',  'bottom': 'jeans', 'height': 1.76})),
#    ('female', Datum({'hair': 'long',  'top': 'sweater', 'bottom': 'skirt', 'height': 1.52})),
    ]
```

male、femaleのサンプルそれぞれ3つずつ用意し、どの特徴がどの程度分類に寄与するかをJubatusに学習させています。
hair longはfemaleに現れやすい、とかbottom skirtはmaleには現れていない、などデータから分類の規則を学んでいきます。

分析させているデータは以下のようなものです。

```
test_data = [
    Datum({'hair': 'short', 'top': 'T shirt', 'bottom': 'jeans', 'height': 1.81}),
    Datum({'hair': 'long',  'top': 'shirt',   'bottom': 'skirt', 'height': 1.50}),
]
```

この分析データがそれぞれmaleと分類できるかfemaleと分類できるかjubatusがスコアをつけて返してきています。
スコアが高いほどそのラベルと言えます。先ほど実行した例では1番目のデータはmaleっぽい、2番目のデータはfemaleっぽいという結果が得られています。
コメントアウトされているデータを学習させてみたり、分析対象のデータを増やしてみたりしていろいろ試してみてください。


# movielens
次に動かすexampleはmovielensです。
今度はjubatusのrecommenderを使ったexampleになります。
recommenderは学習したデータから類似したデータを見つけてくる機能を提供します。
また、類似したデータを使って、データ中の欠損した値の補完する機能もあったりします。

このexampleでは映画の評価データを集めたデータセット[movielens](http://grouplens.org/datasets/movielens/)を使って、映画の好みが似ているユーザを見つけます。

## とりあえず動かしてみる

### データの準備
はじめにデータセットをダウンロードしてくる必要があります。
READMEに沿ってdat ディレクトリの中にデータをダウンロードします。

```
mkdir dat
cd dat
wget http://www.grouplens.org/system/files/ml-100k.zip
unzip ml-100k.zip
cd ..
```

### jubatusサーバの起動
つづいてjubatusサーバを起動します。shogun, genderとは異なり、今度は`jubarecommender`コマンドを実行します。

```
$ jubarecommender -f config.json
2016-12-01 13:53:17,302 5336 INFO  [server_util.cpp:429] starting jubarecommender 1.0.0 RPC server at 10.0.2.15:9199
    pid                  : 5336
    user                 : TkrUdagawa
    mode                 : standalone mode
    timeout              : 10
    thread               : 2
    datadir              : /tmp
    logdir               : 
    log config           : 
    zookeeper            : 
    name                 : 
    interval sec         : 16
    interval count       : 512
    zookeeper timeout    : 10
    interconnect timeout : 10

2016-12-01 13:53:17,304 5336 INFO  [server_util.cpp:165] load config from local file: /home/TkrUdagawa/jubatus-example/movielens/config.json
2016-12-01 13:53:17,304 5336 INFO  [recommender_serv.cpp:118] config loaded: {
  "converter" : {
    "string_filter_types": {},
    "string_filter_rules":[],
    "num_filter_types": {},
    "num_filter_rules": [],
    "string_types": {},
    "string_rules":[
      {"key" : "*", "type" : "str", "sample_weight":"bin", "global_weight" : "bin"}
    ],
    "num_types": {},
    "num_rules": [
      {"key" : "*", "type" : "num"}
    ]
  },
  "parameter" : {
    "hash_num" : 128
    },
  "method": "lsh"
}

2016-12-01 13:53:17,305 5336 INFO  [server_helper.hpp:226] start listening at port 9199
2016-12-01 13:53:17,306 5336 INFO  [server_helper.hpp:233] jubarecommender RPC server startup


```
configの`method`が今度はlshになっています。これはjubarecommenderで利用するアルゴリズムでLocality Sensitive Hash という方法を使うことを意味します。詳細は公式ドキュメントの[クライアントAPI](http://jubat.us/ja/api/api_regression.html)や[アルゴリズム](http://jubat.us/ja/method.html#recommender) をご覧ください。


### クライアントの実行
`jubatus-example/movielens/python` にある学習用スクリプトml_update.pyと分析用スクリプトml_analysis.pyを順に実行していきます。

```
$ python ml_update.py
0
1000
2000
(中略)
99000

$ python ml_analysis.py
user  0  is similar to : []
user  1  is similar to : ['id_with_score{id: 1, score: 1.0}', 'id_with_score{id: 59, score: 0.765625}', 'id_with_score{id: 561, score: 0.765625}', 'id_with_score{id: 757, score: 0.75}', 'id_with_score{id: 64, score: 0.75}', ...
user  2  is similar to : ['id_with_score{id: 2, score: 1.0}', 'id_with_score{id: 701, score: 0.7421875}', 'id_with_score{id: 768, score: 0.71875}', 'id_with_score{id: 624, score: 0.71875}', 'id_with_score{id: 937, score: 0.7109375}, ...
...
user  942  is similar to : ['id_with_score{id: 942, score: 1.0}', 'id_with_score{id: 91, score: 0.703125}', 'id_with_score{id: 724, score: 0.703125}', 'id_with_score{id: 668, score: 0.6953125}',
```

ユーザ1からユーザ942までそれぞれ類似度が高いユーザのIDとスコアが表示されます。例えばユーザ1はユーザ59と最も類似していて、類似スコアは0.765625です。jubarecommenderの仕様上、返却値に自分自身が含まれるため、最も類似度が高い相手として自分自身が表示されていることに注意してください。
また、最初の行でユーザ0の結果が空なのは、存在しないユーザIDに対してクエリを発行してしまっているためです（スクリプトのバグです）。

### ちょっとした解説

解凍したdat/ml-100k/の中を見てもらえばわかりますが、このデータセットの中には何種類かのデータが入っています。今回のexampleでは`u.data`を使用しています。これはuser_id, item_id, rating, timestampの組、つまり、誰がどの映画にどんな評価をいつつけたのか、のデータを格納したtsvファイルです。

学習スクリプトの中ではこのデータを順番に読み込み誰がどの映画にどんな評価をつけたのかを学習させています。

```python:ml_update.py
for line in open('../dat/ml-100k/u.data'):
    userid, movieid, rating, mtime = line[:-1].split('\t')
    datum = Datum({str(movieid): float(rating)})
    recommender.update_row(userid, datum)
```

Datumを作成するときには映画のタイトルをkey, 映画に対する評価(1 ~ 5)をvalueとして与えています。次に、`recommender.update_row(userid, datum)`というメソッドでjubatusに学習させています。`update_row`は第一引数に学習させる行のID, 第二引数に学習させるDatumを与えます。IDが存在しない場合には新たにそのIDが追加され、datumの中身が学習されます。既にIDが存在する場合、datumの中身が追記されていきます。

分析を行う際には`recommender.similar_row_from_id`メソッドを呼び出しています。

```python:ml_analysis.py
for i in range(0,943):
    sr = recommender.similar_row_from_id(str(i) , 10);
    print("user ", str(i),  " is similar to :", list(map(str, sr)))
```

このメソッドは第一引数に学習済みのIDを指定し、第二引数に返す点の数を指定します。このexampleではユーザ0からユーザ942まで順番に、類似しているユーザ10人を探してきています。

近傍点を探す方法はjubatusのコンフィグファイルで指定されています。

```
  "parameter" : {
    "hash_num" : 128
    },
  "method": "lsh"
```

今回用いているアルゴリズムではデータ点を128bitのビットベクトルに押しつぶして類似度の計算をしています。
この`hash_num`を変えたり、`method`を違うものにしたりするとまた違う結果が得られます。

# npb_similar_player

続いてはnpb_similar_playerを実行していきます。
これは似ているプロ野球選手を探すrecommenderのexampleです。
データはプロ野球データfreak(http://baseball-data.com/)から取得した野手の成績データを用いています。

## とりあえず動かしてみる

### jubatusサーバの起動

```
$ jubarecommender -f npb_similar_player.json
```

### クライアントの実行
jubatus-example/npb_similar_player/pythonのupdate.pyとanalyze.pyを順番に実行していきます。

```
$ python update.py
$ python analyze.py
player 山田哲人 is similar to : 山田哲人, 糸井嘉男, 鳥谷敬
player 今宮健太 is similar to : 今宮健太, 荒木雅博, 渡辺直人
player 菊池涼介 is similar to : 菊池涼介, 藤田一也, 鈴木大地
player 鳥谷敬 is similar to : 鳥谷敬, 丸佳浩, 平田良介
...
player 荻野貴司 is similar to : 荻野貴司, 片岡治大, 荒波翔
```

学習と分析がうまくいくと、上記のように類似した選手の名前が表示されます。


## とりあえず動かしてみる(normalize plugin version)
このexampleでは数値データの正規化を行うプラグインが実装されています。ホームラン、打点、打率などデータの範囲が大幅に異なるデータが混ざっているため、正規化したほうが正しい結果が得られるかもしれません。

### 正規化プラグインのビルド
正規化プラグインはjubatus-example/npb_similar_player/normalize_pluginにあります。このディレクトリでpluginのビルドを行ったあと、jubatusのプラグインディレクトリに配置します。

```
$ cd jubatus-example/npb_similar_player/normalize_plugin
$ ./waf configure
$ ./waf build
$ cp build/src/*.so /opt/jubatus/lib/jubatus/plugin/
```

**注：このwafはpython3系では動きません。。。**


### jubatusサーバの起動
今度は`npb_similar_player.plugin.json`を使ってjubatusサーバを起動します。

```
$ jubarecommender -f npb_similar_player.plugin.json
```

コンフィグファイルは以下のようになります。
`num_type` で独自プラグインを読み込んだ2種類の特徴抽出方法を作成しています。
そして、`num_rules`でどのkeyが来たらどの特徴抽出を行うかを設定しています。

```None:npb_similar_player.plugin.json
{
  "method": "inverted_index",
  "converter": {
    "string_filter_types": {},
    "string_filter_rules": [],
    "num_filter_types": {},
    "num_filter_rules": [],
    "string_types": {},
    "string_rules": [],
    "num_types": {
      "HOMERUN": {
        "method": "dynamic",
        "path": "libnormalize_num_feature.so",
        "function": "create",
        "max": "25.0",
        "min": "0.0"
      },
      "BASE": {
        "method": "dynamic",
        "path": "libnormalize_num_feature.so",
        "function": "create",
        "max": "100.0",
        "min": "0.0"
      }
    },
    "num_rules": [
      {"key" : "*率", "type" : "num"},
      {"key" : "本塁打", "type" : "HOMERUN"},
      {"key" : "安打", "type" : "BASE"},
      {"key" : "打点", "type" : "BASE"},
      {"key" : "盗塁", "type" : "BASE"},
      {"key" : "四球", "type" : "BASE"},
      {"key" : "三振", "type" : "BASE"},
      {"key" : "犠打", "type" : "BASE"}
    ]
  },
  "parameter": {}
}

```

### クライアントの実行

```
$ python update.py
$ python analyze.py
player 山田哲人 is similar to : 山田哲人, バルディリス, 村田修一
player 今宮健太 is similar to : 今宮健太, 山崎憲晴, 伊藤光
player 菊池涼介 is similar to : 菊池涼介, 片岡治大, 藤田一也
player 鳥谷敬 is similar to : 鳥谷敬, 角中勝也, 岡島豪郎
...
player 荻野貴司 is similar to : 荻野貴司, 川端崇義, 荒波翔
```

先ほどと結果が変わっていますね。

## ちょっとした解説
大体は先ほどのmovielensと同様です。csvファイルを順番に読み込んでDatumを作り、`update_row`で学習させています。

```python:update.py
   for line in open('../dat/baseball.csv'):
        pname, team, bave, games, pa, atbat, hit, homerun, runsbat, stolen, bob, hbp, strikeout, sacrifice, dp, slg, obp, ops, rc27, xr27 = line[:-1].split(',')
        d = Datum({
          "チーム": team,
          "打率": float(bave),
          "試合数": float(games),
          "打席": float(pa),
          "打数": float(atbat),
          "安打": float(hit),
          "本塁打": float(homerun),
          "打点": float(runsbat),
          "盗塁": float(stolen),
          "四球": float(bob),
          "死球": float(hbp),
          "三振": float(strikeout),
          "犠打": float(sacrifice),
          "併殺打": float(dp),
          "長打率": float(slg),
          "出塁率": float(obp),
          "OPS": float(ops),
          "RC27": float(rc27),
          "XR27": float(xr27)
          })
        recommender.update_row(pname, d)
```

分析するときはファイルから選手名を取ってきて、`similar_row_from_id`を呼び出します。

```python:analyze.py
    for line in codecs.open('../dat/baseball.csv', 'r', 'utf-8'):
      pname, team, bave, games, pa, atbat, hit, homerun, runsbat, stolen, bob, hbp, strikeout, sacrifice, dp, slg, obp, ops, rc27, xr27 = line[:-1].split(',')
      sr = recommender.similar_row_from_id(pname , 4)
```

特徴抽出方法はjubatusのサーバ側で設定しているため、正規化プラグインを使う場合も使わない場合もクライアントプログラム側には変更ありません。


記事が長くなってきたので一旦区切ります。
明日も僕がjubatus-exampleを動かしていきます。