cuMLのSVCをGridsearchCVの識別器として使う方法

# 記事の概要
scikit-learnのGridsearchCVにcuMLのSVM（SVC）をestimatorとして渡すと，
エラーが発生したので解決策を残します．
Qiita初投稿なので，間違いやわかりにくい点などあればご指摘いただけると有難いです．

# 先に結論だけ
### エラーの原因
- scikit-learnのSVM.SVC.predict()の戻り値はnumpyの配列
- cuMLのcuml.svm.SVC.predict()の戻り値はcuDFのSeries

scikit-learnのGridsearchCVは，estimator.predict()の戻り値としてnumpy配列を想定しています．
しかし，cuMLのSVC.predict()の戻り値はcuDFのSeriesなので，GridsearchCVの内部でエラーが発生します．

GridsearchCVを使わない場合，戻り値を都度numpy配列に変換することで解決できますが，GridsearchCVを使う場合，その手は使えません．
（SVCクラスのインスタンスごとGridsearchCVに渡す必要があるため）

### 解決策
- cuml.svm.SVCを継承したクラスを作成
- predictメソッドをオーバーライドして，戻り値をnumpy配列に直してから出力するようにする
- そのクラスのインスタンスをestimatorとして使う

# 実装例
今回は例として，SVMを使ってMNISTデータセットの「5」と「8」を分類します．
理由は以下のような感じです．

- MNISTは入手や整形が楽，データ数もちょうど良い
- cuMLのSVCは今のところ2クラス分類にしか対応していない
- 「5」と「8」の分類が一番難しいらしい（[参考](https://note.nkmk.me/python-scikit-learn-svm-mnist/)）

## 実行環境
- OS: Ubuntu 18.04.2 LTS
- CPU: Intel Xeon W-2133
- GPU: GeForce RTX 2080 Ti 
- python: 3.6.5
- CUDAのバージョン: 10.0.130
- cuMLのバージョン: 0.12
- scikit-learnのバージョン: 0.22.1   

## データセット作成
まずはデータセット作りから．
MNISTの5と8だけを取り出して，
ラベルを2値（5→0，8→1）に直します．

```python:dataset_maker.py
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split

def dataset_maker():
    mnist = fetch_openml('mnist_784', version=1)
    data_58 = []
    label_58 =[]
    for data,target in zip(mnist.data, mnist.target):
        if target=='5':
            data_58.append(data/255)
            label_58.append(0)
        elif target=='8':
            data_58.append(data/255)
            label_58.append(1)

    data_58 = np.array(data_58)
    label_58 = np.array(label_58)
    X_train, X_test, y_train, y_test = train_test_split(data_58, label_58)

    return X_train, X_test, y_train, y_test
```

## __sklearn.svm.SVC.predict()__ と **cuml.svm.SVC.predict()** の差異

エラーの原因である，predictメソッドの戻り値の違いについて確認します．
下のコードのように，基本的にcuMLのSVCはsklearnのSVCと同様に扱えます．導入が簡単で嬉しい．

```python:sklearn_vs_cuML.py
from sklearn.svm import SVC as skSVC
from cuml.svm import SVC as cuSVC

def classify_sklearn(X_train, X_test, y_train, y_test):
    clf = skSVC()
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)

    print("skSVC output_type:{}".format(type(y_pred)))
    print("skSVC y_pred:{}".format(y_pred[0:10]))

def classify_cuml(X_train, X_test, y_train, y_test):
    clf = cuSVC()
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)

    print("cuSVC output_type:{}".format(type(y_pred)))
    print("cuSVC y_pred:{}".format(y_pred[0:10]))


if __name__ == "__main__":
    X_train, X_test, y_train, y_test = dataset_maker()
    classify_sklearn(X_train, X_test, y_train, y_test)
    classify_cuml(X_train, X_test, y_train, y_test)
```

これを実行すると，出力は以下のようになります．

```
skSVC output_type:<class 'numpy.ndarray'>
skSVC y_pred:[0 0 0 1 0 0 0 0 1 0]
cuSVC output_type:<class 'cudf.core.series.Series'>
cuSVC y_pred:0    0.0
1    0.0
2    0.0
3    1.0
4    0.0
5    0.0
6    0.0
7    0.0
8    1.0
9    0.0
dtype: float64
```
上で書いたように，戻り値が違うことがわかります．
まとめるとこんな感じです．

- 出力の型が違う
    - sklearn: numpy.ndarray
    - cuML: cudf.core.series.Series
- 出力された配列における要素の型が違う
    - sklearn: int
    - cuML: float64

このうち前者が原因で，cuml.svm.SVC.predict()の戻り値をsklearnの評価関数にそのまま渡すと，
`ValueError: Expected array-like (array or non-string sequence)` と怒られます．[^1]

[^1]: 後者は勝手にキャストされるようで，前者だけ直せば動きます．とはいえ気持ち悪いので，下に示すコードでは明示的にint型へキャストしています．

これ自体はnumpy配列に直してあげれば解決するので，cuMLのSVCで分類した時は，
predictメソッドの戻り値を[cudf.core.series.Series.to_array()](https://rapidsai.github.io/projects/cudf/en/latest/api.html#cudf.core.series.Series.to_array)を用いてnumpy配列に変換してから，
scikit-learnの評価関数に渡しましょう．[^2] 

[^2]: cuMLの評価関数はもちろんcuDFのSeriesに対応していますが，現状種類が少なく，実用上はおそらくscikit-learnの評価関数を使う場合が多いんじゃないかと思います．

## cuMLでGridsearchCVを使う
さて本題です．
SVCのハイパーパラメータをグリッドサーチで決定したい場合，
おそらくscikit-learnのGridsearchCVを使う方法がまず思い浮かぶと思います．
まずは，scikit-learnのSVCをestimatorとしてやってみます．

```python:classify_sklearn_grid.py
def classify_sklearn_grid(X_train, X_test, y_train, y_test):
    parameters = {'kernel': ['linear', 'rbf'],
                  'C': [0.1, 1, 10, 100],
                  'gamma': [0.1, 1, 10]}

    clf = GridSearchCV(skSVC(), parameters, scoring='accuracy', verbose=2)
    clf.fit(X_train, y_train)
    y_pred = clf.best_estimator_.predict()

if __name__ == "__main__":
    X_train, X_test, y_train, y_test = dataset_maker()
    pred_sk_grid = classify_sklearn_grid(X_train, X_test, y_train, y_test)
```
このようになると思います．

cuMLのSVCは.fit()や.predict()などの必要なメソッドを備えたクラスなので，GridsearchCVのestimatorとしての用件を満たしています．

しかし，実際にはpredictメソッドの戻り値がcuDFのSeriesであるため，結果の評価をするプロセスでエラーを引き起こします．
GridsearchCVにはSVCのインスタンスごと渡す必要があるので，predictメソッドが呼び出されるごとにto_arrayメソッドを使って変換することもできません．

この問題を解決するには，戻り値がnumpy配列になるようpredictメソッドをオーバーライドすればOKです．

具体的に説明していきます．
といっても簡単で，以下のような新しいクラスを定義するだけです．

```python:MySVC.py
from cuml.svm import SVC

class MySVC(SVC):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
    def predict(self, X):
        y_pred = super().predict(X).to_array().astype(int)
        return y_pred
```
このMySVCをcumlのSVCの代わりにGridsearchCVに渡せばOKです．
書く必要もない気がしますが，

```python:classify_MySVC.py
from MySVC import MySVC

def classify_cuml_grid(X_train, X_test, y_train, y_test):

    parameters = {'kernel': ['linear', 'rbf'],
                  'C': [0.1, 1, 10, 100],
                  'gamma': [0.1, 1, 10]}

    clf = GridSearchCV(MySVC(), parameters, scoring='accuracy', verbose=2)
    clf.fit(X_train, y_train)
    y_pred = clf.best_estimator_.predict(X_test)

    return y_pred

if __name__ == "__main__":
    X_train, X_test, y_train, y_test = dataset_maker()
    pred_cu_grid = classify_cuml_grid(X_train, X_test, y_train, y_test)
```

こんな感じです．
これでcuMLでGridsearchCVを使うことができるようになるはずです！
長くなりましたが，読んでいただきありがとうございました！

# おまけ
せっかくなので，scikit-learnを使った場合とcuMLを使った場合の実行時間の差を載せておきます．

- scikit-learn: 1348.87 [s]（約22.5分）
- cuML: 270.06 [s]（約4.5分）

試行は各1回だけなので参考程度ですが，scikit-learnだと約5倍くらい時間がかかってしまいました．
cuMLはやっぱり速い！


# 参考文献
- [scikit-learn API - GridSearchCV](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html)
- [scikit-learn API - SVC](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html#sklearn.svm.SVC)
- [cuML API Reference](https://rapidsai.github.io/projects/cuml/en/latest/api.html)
- [scikit-learnのSVMでMNISTの手書き数字データを分類](https://note.nkmk.me/python-scikit-learn-svm-mnist/)