Jubakitを使ってみました

vol03_01_fig001

こんにちは、小澤です。
この記事はJubatus Advent Calendar18日目のものとなります。

今回はJubakitというものを使ってみたいと思います。

Jubatkitとは

JubakitのGithubによると

jubakit is a Python module to access Jubatus features easily. jubakit can be used in conjunction with scikit-learn so that you can use powerful features like cross validation and model evaluation.

となっています。

Jubatusは分散処理による機械学習を実現するために、Client-Serverモデルとなっています。
また、オンライン学習という性質もあり、まとまったデータでモデルを作成して評価して・・・といったことを行うというよりは、本番環境での動作を意識した際には非常に使いやすいものになっています。

しかし、一方で利用するアルゴリズムやハイパーパラメータは実行時に指定する設定ファイルに記述することから、モデル比較やパラメータチューニングなどは決して行いやすいものとは言い難い部分もあります。
また、近年ではscikit-learnやSparkのMLlibなどPipelineという形でこれらの実用的なプロセス一連の流れを定義可能なものもあり、それらと比較すると作成したモデルが本番運用に利用な条件に達しているかの判断が難しいというハードルもありました。

Jubakitはそれらの問題を解決するアプローチとして有用なものになるかと思います。

learn

Jubakitのインストール

まずはJubakitのインストールを行います。 JubakitはPythonから利用することになるため、Pythonのインストールが必要です。 また、Jubatusと一部の機能でscikit-learnに依存しています。
今回はUbuntu14.04環境にAnacodaとJubatus, Jubakitにインストールを行います。

# Anacodaのインストール
wget https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh
bash Anaconda3-4.2.0-Linux-x86_64.sh

# Jubatusのインストール
sudo sh -c "echo 'deb http://download.jubat.us/apt/ubuntu/trusty binary/' >> /etc/apt/sources.list.d/jubatus.list"
sudo apt-get update
sudo apt-get install jubatus
source /opt/jubatus/profile
# 毎回pathを通す必要がないように.bashrcにも追記しておく
echo "source /opt/jubatus/profile" >> .bashrc

# PythonのJubatusクライアントとJubakitのインストール
pip install jubatus
pip install jubakit

これで必要な環境はそろいました。
なお、今回利用するPythonのバージョンは3.5となっています。

Jubakitを動かしてみる

Quick Start

まずはQuick Startを動かしてみます。

ソース中で利用しているデータは公式のexampleに含まれているirisデータセットとなっているのでそちらをお使いください。

from jubakit.classifier import Classifier, Schema, Dataset, Config
from jubakit.loader.csv import CSVLoader

# csvファイルとなっているため、CSVLoaderを利用
loader = CSVLoader('iris.csv')

# データのスキーマの定義
# irisの場合、予測対象となるSpeciesがlabel, それ以外は数値
schema = Schema({
  'Species': Schema.LABEL,
}, Schema.NUMBER)

# ローダとスキーマを指定してデータセットを定義
# irisの場合予測ラベルごとにデータが並んでいるが
# オンライン学習ではデータの順番に結果が依存するため全体をシャッフルする
dataset = Dataset(loader, schema).shuffle()

# jubaclassifierの実行
classifier = Classifier.run(Config())

# 学習を行う
for _ in classifier.train(dataset): pass

# 予測を行って結果を出力
for (idx, label, result) in classifier.classify(dataset):
  print("true label: {0}, estimated label: {1}".format(label, result[0][0]))

実行すると、学習を行った後予測結果の出力を行います。

$ python quick_start.py 
true label: Iris-setosa, estimated label: Iris-setosa
true label: Iris-setosa, estimated label: Iris-setosa
true label: Iris-virginica, estimated label: Iris-versicolor
true label: Iris-versicolor, estimated label: Iris-versicolor
true label: Iris-virginica, estimated label: Iris-virginica
...
...

Cross Validation

次にクロスバリデーションを行うサンプルを見てみます。
なお、クロスバリデーションそのものについての説明はここでは割愛します。

exampleのclassifier_kflod.pyとなります。

import sklearn.datasets
import sklearn.metrics
from sklearn.cross_validation import StratifiedKFold

from jubakit.classifier import Classifier, Dataset, Config

# 今回はcsvからの読み込みではなく、scikit-learnのデータセットに含まれるirisのデータを利用する
iris = sklearn.datasets.load_iris()

# jubakit用のデータセットに変換
#  dataset = Dataset.from_array(iris.data, iris.target)
#  と記述することもできるが、今回は人間が読みやすいようにfeatureとlabelの名前も設定している
dataset = Dataset.from_array(iris.data, iris.target, iris.feature_names, iris.target_names)

dataset = dataset.shuffle()
classifier = Classifier.run(Config())

# 結果を入れておくための変数
true_labels = []
predicted_labels = []

# 10-foldのクロスバリデーション
# StratifiedKFoldにデータセットとfold数を渡してやると、学習用とテスト用に分割されたデータのインデックスが返されるので
# それをfoldの数分ループさせている
for train_idx, test_idx in StratifiedKFold(list(dataset.get_labels()), n_folds=10):
  
  # clearを呼び出して前回の学習したモデルをリセット
  classifier.clear()

  # インデックスの値を利用していデータセットを学習用とテスト用に分割
  (train_ds, test_ds) = (dataset[train_idx], dataset[test_idx])

  # 学習と予測のやり方は前回と同じ
  for (idx, label) in classifier.train(train_ds):
    pass

  for (idx, label, result) in classifier.classify(test_ds):
    # 予測の結果は[ ("label_1", score_1), ("label_2", score_2), ... ]
    # のような形式で、スコアが高い順にソートされているので一番スコアが高い予測のラベルを取得している
    pred_label = result[0][0]

    # 結果を入れておく.
    true_labels.append(label)
    predicted_labels.append(pred_label)

# Stop the classifier.
classifier.stop()

# 結果のレポーティングはscikit-learnのものをそのまま利用
print(sklearn.metrics.classification_report(true_labels, predicted_labels))

実行すると

$ python classifier_kfold.py 
             precision    recall  f1-score   support

     setosa       1.00      1.00      1.00        50
 versicolor       0.50      1.00      0.67        50
  virginica       0.00      0.00      0.00        50

avg / total       0.50      0.67      0.56       150

のように表示されます。 この結果の見方に関しては割愛しますが、クロスバリデーションのためにデータを分割してループさせる部分と結果のレポーティング以外は最初のサンプルとほぼ一緒ですね。

パラメータの最適化

最後にclassifier_hyperopt_tuning.pyを見てみます。 こちらは複数のアルゴリズム、パラメータの組み合わせの中で最適なものを発見するサンプルになっています。
このサンプルはhyperoptというライブラリに依存しているのでそちらもインストールしておきます。

pip install hyperopt

さて、これまでのサンプルと比較して複雑なことをしているため、順を追って見てみましょう。
まずmainの中の全体の流れを見てみます。

if __name__ == '__main__':
  iris = sklearn.datasets.load_iris()
  dataset = Dataset.from_array(iris.data, iris.target, iris.feature_names, iris.target_names)
  dataset = dataset.shuffle()
  
  # パラメータサーチを行う空間を指定
  space = search_space()
  
  # 最適化の方法
  # ベイズ最適化か(tpe.suggest)かランダム(rand.suggest)かを選択できる
  algo = tpe.suggest
  
  # 評価回数
  # 今回の場合だとクロスバリデーションの回数に使用
  max_evals = 10
  
  print(' {0:<6}: {1:<5} ({2})'.format('score', 'algo', 'hyperparameters'))
  
  # 最適化の実行
  best = fmin(function, space, algo=algo, max_evals=max_evals)
  
  print_result(best)

順に見てみます。
まずはsearch_space関数です。 こちらは探索範囲を定義しています。
基本的は、関数の外で定義してあるアルゴリズム、パラメータの範囲で探索範囲として定義しています。 パラメータの範囲指定などはhyperoptの解説となってしまって本筋とは外れるため、docstringにあるようにそちらのwiki情報を参照してください。

# hyperparameter domains 
classifier_types = ['LinearClassifier', 'NearestNeighbor']
# linear classifier hyperparameters
linear_methods = ['AROW', 'CW']
regularization_weight = [0.0001, 1000.0] 
# nearest neighbor classifier hyperparameters
nn_methods = ['lsh', 'euclid_lsh', 'minhash']
nearest_neighbor_num = [2, 100]
local_sensitivity = [0.01, 10]
hash_num = [512, 512] 

def search_space():
  """
  Return hyperparameter space with Hyperopt format.
  References: https://github.com/hyperopt/hyperopt/wiki/FMin
  """
  space = hp.choice('classifier_type', [
      {
         'classifier_type': 'LinearClassifier',
         'linear_method': hp.choice('linear_method', linear_methods),
         'regularization_weight': hp.loguniform('regularization_weight',
                                            np.log(regularization_weight[0]), 
                                            np.log(regularization_weight[1]))
      }, {
         'classifier_type': 'NearestNeighbor',
         'nn_method': hp.choice('nn_method', nn_methods),
         'nearest_neighbor_num': hp.uniform('nearest_neighbor_num', 
                                            nearest_neighbor_num[0], 
                                            nearest_neighbor_num[1]),
         'local_sensitivity': hp.loguniform('local_sensitivity', 
                                            np.log(local_sensitivity[0]), 
                                            np.log(local_sensitivity[1])),
         'hash_num': hp.loguniform('hash_num', np.log(hash_num[0]), np.log(hash_num[1]))
      }
  ])
  
  return space

次にfminに渡しているfunction関数になります。
この関数はこれまでも見てきたjubakitの設定なので難しい点はないかと思います。
cv_score関数は先ほどの例と同様のクロスバリデーションを行う関数となっています。特に違いはないので、問題ないかと思います。
print_log関数はアルゴリズム、パラメータごとのスコアを標準出力しているのみなので、詳細には立ち入りませんが何をしているかはソースを実際に見ていただければそれほど難しくなく理解できるかと思います。

def function(params):
  """
  Function to be optimized.
  """
  config = jubatus_config(params)
  classifier = Classifier.run(config)
  # scikit-learnの関数
  metric = accuracy_score
  
  score = cv_score(classifier, dataset, metric=metric)
  classifier.stop()
  # print score and hyperparameters
  print_log(score, params)
  # hyperoptでは評価値が最小のものを求めるのので-1をかけて順番を逆転させる
  return -1.0 * score

今までと異なる点はconfigに値を渡すjubatus_config関数となります。 こちらは関数の中身を見ると、Jubatusを使ったことがある方ならお気づきになるかと思いますが、起動時に渡すjsonと同等のものです。
このサンプル以外ではconfigの内容は明示しておりませんでしが、ここと同様の記述で他の手法も扱えることがわかるかと思います。
ここからわかるようにjubakitでは、パラメータ変更時などの裏側でのjubatusの設定書き換えや再起動をラップしていくれているようです。

def jubatus_config(params):
  """
  convert hyperopt config to jubatus config
  """
  if params['classifier_type'] == 'LinearClassifier':
    config = Config(method=params['linear_method'], 
    	            parameter={'regularization_weight': params['regularization_weight']})

  elif params['classifier_type'] == 'NearestNeighbor':
    config = Config(method='NN',
                    parameter={'method': params['nn_method'],
                               'nearest_neighbor_num': int(params['nearest_neighbor_num']),
                               'local_sensitivity': params['local_sensitivity'],
                               'parameter': {'hash_num': int(params['hash_num'])}})

  else:
  	raise NotImplementedError()

  return config

最後にfminによる最適化終了後、print_result関数を呼び出しています。 こちらも出力を行うのみとなっているので特別な解説は必要ないかと思います。

このプログラムを実行すると出力は以下のようになります。

$ python classifier_hyperopt_tuning.py 
 score : algo  (hyperparameters)
 0.9133: AROW  (reguralization_weight:22.4794)
 0.9600: NN    (method:minhash, nearest_neighbor_num:63, local_sensitivity:3.1264, hash_num:511)
 0.8933: NN    (method:euclid_lsh, nearest_neighbor_num:41, local_sensitivity:0.0323, hash_num:511)
 0.9267: NN    (method:lsh, nearest_neighbor_num:72, local_sensitivity:0.8923, hash_num:511)
 0.9133: AROW  (reguralization_weight:6.4864)
 0.9667: NN    (method:minhash, nearest_neighbor_num:49, local_sensitivity:0.0785, hash_num:511)
 0.6667: AROW  (reguralization_weight:0.0045)
 0.9533: NN    (method:minhash, nearest_neighbor_num:37, local_sensitivity:0.5177, hash_num:511)
 0.8733: NN    (method:euclid_lsh, nearest_neighbor_num:75, local_sensitivity:0.0745, hash_num:511)
 0.9200: NN    (method:euclid_lsh, nearest_neighbor_num:40, local_sensitivity:8.7407, hash_num:511)
 ------------------------------------------------------------
 best score and hyperparameters
 ------------------------------------------------------------
 0.9667: NN    (method:minhash, nearest_neighbor_num:49, local_sensitivity:0.0785, hash_num:511)

補足

今回のサンプルはいずれもデータのシャッフルなど乱数に依存している部分があります。 そこため、出力結果に関してはここで示したものと毎回厳密に一致しない可能性があります。

終わりに

今回、サンプルソースをもとにJubakitを使ってみました。
Jubatusは学習・予測ともにオンラインであったり、分散処理が可能であったりと他の機械学習ライブラリと比較しても特徴のあるものとなっています。 本番環境への導入がイメージしやすいものとなっている一方で、これまでは検証やプロトタイピング、精度向上のための試行錯誤などが行いづらい側面もありました。
Jubakitを利用すればそういった点もカバー可能になるかと思います。

ただし、注意が必要な点もあります。 オンライン学習ではデータが入力される順番に結果が依存します。 特に直近のデータの影響を強く受けるという点は最新の情報をすぐに反映できるという利点がある一方で、気づかないうちに全く使い物にならない学習をしていたという状況にもなりえます。

いずれにせよ、jubatusや機械学習のシステム導入は強力なツールであるという点については変わりないと思いますで、まずは実際に試してみることから始めてみるのが一番かと思います。

AWS Cloud Roadshow 2017 福岡