Jubatusとscikit-learnを比較してみる

[Jubatus Advent Calendar](http://qiita.com/advent-calendar/2016/jubatus) 22日目です．

Pythonで機械学習をされている方にはおなじみの[scikit-learn](http://scikit-learn.org)とJubatusの性能比較をやってみます．

# 使用するデータセット

今回はscikit-learn API として公開されている5種類のデータセットを利用します．

- iris dataset: みなさんご存知アヤメの種類を判定する数値分類タスクです．
- breast cancer dataset：乳がんが発症するか判定する数値分類タスクです．
- digits dataset: 8x8ピクセルの手書き数字が0-9のどれに該当するかを判定する数値分類タスクです．
- MNIST dataset: 28x28ピクセルの手書き数字が0-9のどれに該当するかを判定する数値分類タスクです．
- 20newsgroups dataset: 約20000件の文書が20カテゴリのどれに属するかを判定するテキスト分類タスクです．

## データの取得と学習・テストデータへの分割

scikit-learn API にてデータを取得した後，学習データとテストデータを2:1の割合で分割します．

```python
def load_dataset():
    from sklearn.datasets import fetch_mldata
    dataset = fetch_mldata('MNIST original')
    X = csr_matrix(dataset.data, dtype=np.float64) 
    y = dataset.target
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
    return X_train, X_test, y_train, y_test
```


# Jubatusを用いた性能評価スクリプト作成
今回の評価では[embedded-jubatus-python](https://github.com/jubatus/embedded-jubatus-python)を使います．
embedded-jubatus-pythonは，Jubatusの学習アルゴリズムをサーバを介さず直で叩くことができます．そのため，手元のマシンでサクッと性能を確かめたいといったライトな使い方に非常に向いています．

## 分類器の作成
まずはJubatusの線形分類器を作成してみましょう．
今回はテキストを重み付けするコンフィグとして，サンプル重みには`log_tf`を，大域重みには`bm25`を利用します．`bm25`については[こちら](http://blog.jubat.us/2016/06/jubatus-092-released.html)のブログをご覧ください．
`linear_classifier(...)`はJubatusの線形分類器を起動する関数です．`perceptron`, `PA`とその他の線形分類器はコンフィグが異なりますので場合分けをしています．

```python
import copy
from embedded_jubatus import Classifier

# Default Config
CONFIG = {
    'method': 'perceptron',
    'converter': {
        'num_filter_types': {},
        'num_filter_values': [],
        'string_filter_types': {},
        'string_filter_values': [],
        'num_types': {},
        'num_rules': [
            {'key': '*', 'type': 'num'}
        ],
        'string_types': {},
        'string_rules': [
            {'key': '*', 'type': 'space', 'sample_weight': 'log_tf', 'global_weight': 'bm25'}
        ]
    },
    'parameter': {}
}

def linear_classifier(method='AROW', regularization_weight=1.0):
    """ 線形分類器を起動する """
    cfg = copy.deepcopy(CONFIG)
    cfg['method'] = method
    if method not in ('perceptron', 'PA'):  # perceptron, PA 以外はパラメータが必要
        cfg['parameter']['regularization_weight'] = regularization_weight
    return Classifier(cfg)

```

無事，分類器を作成する関数が書けました．

## データを学習して評価する
続いて，分類器にデータを学習させて評価する関数を書いてみましょう．Jubatusはデータの投入順序で結果がかわるので，4試行平均を取ることにします．数値分類とテキスト分類で書き方が変わりますので，分けて紹介します．

### 数値分類

実はembedded-jubatus-pythonは`fit(X, y)`関数と`predict(X)`関数が用意されているため，scikit-learnライクに記述することができます．これなら簡単に使えますね ;)

```python
def score(clf, X_train, X_test, y_train, y_test): 
    clf.fit(X_train, y_train)                                  # 学習する
    test_score = accuracy_score(y_test, clf.predict(X_test))   # 予測して正解率を求める
    return test_score

def evaluate(X_train, X_test, y_train, y_test, n_trials=4):
    jubatus_methods = ['perceptron', 'PA', 'PA1', 'PA2', 'CW', 'AROW', 'NHERD']
    results = dict.fromkeys(jubatus_methods, 0)
    for i in range(n_trials): 
        X_train, y_train = shuffle(X_train, y_train, random_state=42)  # データをシャッフル
        for method in jubatus_methods:
            clf = linear_classifier(method=method)                     # 線形分類器の起動
            test_score = score(clf, X_train, X_test, y_train, y_test)  # 学習・テストして正解率を求める
            print('{0:.3f}\t{1}'.format(test_score, method))
            results[method] += test_score
    results = {k: v / n_trials for k, v in results.items()}            # 各分類器の4試行平均の正解率を求める
```


### テキスト分類

embedded-jubatus-pythonの`fit(X, y)`関数，`predict(X)`関数は`numpy.float64`タイプの数値データしか受け付けません．そのため，テキスト分類ではjubatus固有のデータ型`Datum`を用いて学習とテストを行います．
といっても面倒なことはなく`score(...)`関数を書き換えてあげれば，先程の数値分類と同じコードを使いまわすことができます．

```python
def score(clf, X_train, X_test, y_train, y_test): 
    train_data = [(yi, Datum({'message': xi})) for (xi, yi) in zip(X_train, y_train)] # 教師データを「
    test_data = [Datum({'message': xi}) for xi in X_test]
    clf.train(train_data)
    predictions = clf.classify(test_data)
    y_pred = [max(pred, key=lambda x:x.score).label for pred in predictions]
    return accuracy_score(y_test, y_pred)
```


# scikit-learnを用いた性能評価スクリプト作成
scikit-learnでも基本的にjubatusと同じスクリプトを使いまわします．

## 分類器の作成
scikit-learnでも同様にオンライン線形分類器を使います．
Perceptron, PA1, PA2はJubatusにも入っていますね．オンライン線形SVM(LSVM)，オンラインロジスティクス回帰(LR)は，確率的勾配降下法を用いてそれぞれの損失関数を最小化することで分類問題をオンラインで解きます．

```python
def sklearn_linear_classifier(method='Perceptron(sk)'):
    sgd_params = {'penalty': 'l2', 'n_iter':1, 'shuffle': False, 'random_state': 42}
    pa_params = {'C': 1.0, 'n_iter': 1, 'shuffle': False, 'random_state': 42}
    if method == 'Perceptron(sk)':
        return Perceptron(n_iter=1, shuffle=False, random_state=42)
    elif method == 'LSVM(sk)':
        return SGDClassifier(loss='hinge', **sgd_params)
    elif method == 'LR(sk)':
        return SGDClassifier(loss='log', **sgd_params)
    elif method == 'PA1(sk)':
        return PassiveAggressiveClassifier(loss='hinge', **pa_params)
    elif method == 'PA2(sk)':
        return PassiveAggressiveClassifier(loss='squared_hinge', **pa_params)
    else:
        raise NotImprementedError()
```

## データを学習して評価する

### 数値分類
数値分類はほぼjubatusと同じです．

```python
def score(clf, X_train, X_test, y_train, y_test): 
    clf.fit(X_train, y_train)                                  # 学習する
    test_score = accuracy_score(y_test, clf.predict(X_test))   # 予測して正解率を求める
    return test_score

def evaluate(X_train, X_test, y_train, y_test, n_trials=4):
    sklearn_methods = ['Perceptron(sk)', 'PA1(sk)', 'PA2(sk)', 'LSVM(sk)', 'LR(sk)']
    results = dict.fromkeys(sklearn_methods, 0)
    for i in range(n_trials): 
        X_train, y_train = shuffle(X_train, y_train, random_state=42)  # データをシャッフル
        for method in jubatus_methods:
            clf = sklearn_linear_classifier(method=method)                     # 線形分類器の起動
            test_score = score(clf, X_train, X_test, y_train, y_test)  # 学習・テストして正解率を求める
            print('{0:.3f}\t{1}'.format(test_score, method))
            results[method] += test_score
    results = {k: v / n_trials for k, v in results.items()}            # 各分類器の4試行平均の正解率を求める
```


### テキスト分類
scikit-learn はテキストデータをそのまま食わせることはできませんので，`TfidfVectorizer`を使ってテキストを数値に変換してから学習を行います（`TfidfVectorizer`はバッチ処理なため，scikit-learnの方が有利な条件となってしまいますが…）

```python
def score(clf, X_train, X_test, y_train, y_test): 
    vectorizer = TfidfVectorizer()
    X_train = vectorizer.fit_transform(X_train)
    X_test = vectorizer.transform(X_test)
    clf.fit(X_train, y_train)                                  # 学習する
    test_score = accuracy_score(y_test, clf.predict(X_test))   # 予測して正解率を求める
    return test_score
```

以上で性能評価スクリプトが書き終わりました．

# 実験結果

各データで評価した結果を以下に示します．太字が各データセットでのベスト性能です．
news20データでは少し負けてしまいましたが，数値データだと常にJubatusが勝っています！

| library  | classifier | iris | cancer | digits | mnist | news20 |
|:--------:|:------------:|:------------:|:------------:|:------------:|:------------:|:------------:|
| jubatus  | AROW | 0.880  | **0.910**  | **0.941**  | **0.891**  | 0.840 |
| jubatus  | CW | **0.885**  | 0.904  | 0.870  | 0.100  | 0.835 |
| jubatus  | NHERD | 0.675  | 0.356  | 0.475  | 0.125  | 0.512 |
| jubatus  | PA | 0.690  | 0.681  | 0.931  | 0.870  | 0.824 |
| jubatus  | PA1 | 0.690  | 0.681  | 0.931  | 0.870  | 0.824 |
| jubatus  | PA2 | 0.690  | 0.681  | 0.931  | 0.870  | 0.826 |
| jubatus  | perceptron | 0.650  | 0.751  | 0.830  | 0.872  | 0.713 |
| sklearn  | LR  | 0.505  | 0.751  | 0.886  | 0.861  | 0.821 |
| sklearn  | LSVM  | 0.595  | 0.694  | 0.879  | 0.854  | 0.833 |
| sklearn  | PA1  | 0.690  | 0.682  | 0.923  | 0.865  | 0.845 |
| sklearn  | PA2  | 0.690  | 0.682  | 0.923  | 0.865  | **0.846** |
| sklearn  | Perceptron | 0.595  | 0.694  | 0.878  | 0.854  | 0.764 |

数値データ分類だとscikit-learnとほぼ同じように書けますので，ぜひJubatusを使ってみてくださいね！

今回使ったソースコードは[こちら](https://github.com/shiodat/jubatus-advent-calendar/tree/master/performance)においてあるので，ご自由にお使いください．