記事の概要
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」の分類が一番難しいらしい(参考)
実行環境
- 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)に直します.
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と同様に扱えます.導入が簡単で嬉しい.
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
これ自体はnumpy配列に直してあげれば解決するので,cuMLのSVCで分類した時は,
predictメソッドの戻り値をcudf.core.series.Series.to_array()を用いてnumpy配列に変換してから,
scikit-learnの評価関数に渡しましょう.2
cuMLでGridsearchCVを使う
さて本題です.
SVCのハイパーパラメータをグリッドサーチで決定したい場合,
おそらくscikit-learnのGridsearchCVを使う方法がまず思い浮かぶと思います.
まずは,scikit-learnのSVCをestimatorとしてやってみます.
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です.
具体的に説明していきます.
といっても簡単で,以下のような新しいクラスを定義するだけです.
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です.
書く必要もない気がしますが,
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
- scikit-learn API - SVC
- cuML API Reference
- scikit-learnのSVMでMNISTの手書き数字データを分類