CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: scikit-learn の Pipeline を使ってみる

機械学習では、元のデータセットに対して前処理や推論フェーズが何段にも重なることがある。 scikit-learn には、そういった何段にも重なった処理を表現しやすくするために Pipeline という機能が備わっている。 今回は、その Pipeline を使ってみることにする。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V                           
Python 3.6.5

下準備

まずは scikit-learn と必要なパッケージをあらかじめインストールしておく。

$ pip install numpy scipy scikit-learn

続いて Python のインタプリタを起動しておく。

$ python

データセットについては Iris データセットを使う。

>>> from sklearn import datasets
>>> iris = datasets.load_iris()
>>> X, y = iris.data, iris.target

これで下準備はできた。

Pipeline 機能を使ってみる

Pipeline では、その名のごとく一連の処理をパイプラインで表現する。 具体的には、リストに scikit-learn のオブジェクトを適用する順番で格納しておく。

以下にサンプルコードのパイプラインを示す。 このパイプラインでは、まず前処理として主成分分析 (PCA) した上で、それをランダムフォレストで分類できる。 リストには処理の名前と、その処理を実行するオブジェクトを対にしたタプルを入れる。

>>> from sklearn.pipeline import Pipeline
>>> from sklearn.decomposition import PCA
>>> from sklearn.ensemble import RandomForestClassifier
>>> steps = [
...     ('pca', PCA()),
...     ('rf', RandomForestClassifier())
... ]
>>> pipeline = Pipeline(steps=steps)

PCA とランダムフォレストを選んだこと自体には特に意味はなくて、あくまでこんな風にできるよという例。

Pipeline オブジェクトは scikit-learn のオブジェクトが一般的に持っているインターフェースを備えている。 なので、以下のような感じで fit() メソッドで学習した上で predict() メソッドで推論、といういつもの流れが使える。

>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
>>> pipeline.fit(X_train, y_train)
>>> y_pred = pipeline.predict(X_test)

精度自体は今回の目的ではないけど、一応計算しておく。

>>> from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
0.98

GridSearchCV と組み合わせて使う

Pipeline は GridSearchCV と組み合わせることもできる。 GridSearchCV を使うと、ハイパーパラメータの中から性能の良い組み合わせを自動的に探してくれる。

Pipeline と GridSearchCV を組み合わせて使うときは、パラメータ名の指定に工夫が必要になる。 具体的には <処理の名前>__<パラメータ名> という形式で指定する。

>>> from sklearn.model_selection import GridSearchCV
>>> params = {
...     'pca__n_components': range(1, X.shape[1]),
...     'rf__n_estimators': [10, 100, 1000],
... }
>>> grid_search_cv = GridSearchCV(pipeline,  param_grid=params, cv=4)

ふつうの scikit-learn オブジェクトと GridSearchCV を組み合わせるときは単純にパラメータ名だけで良かった。 それに対して Pipeline には複数のオブジェクトが含まれるため、それを識別するための名前が追加で必要になる。

GridSearchCV#fit() メソッドでハイパーパラメータの探索処理が走る。

>>> grid_search_cv.fit(X, y)

処理が終わったら best_params_ を参照することで最適なパラメータの値が分かる。

>>> grid_search_cv.best_params_
{'pca__n_components': 2, 'rf__n_estimators': 1000}

Pipeline に組み込むオブジェクトを自作する

ここまでは scikit-learn オブジェクトをそのまま Pipeline に組み込むパターンを試した。 次は Pipeline に自作した前処理や推論フェーズの処理を組み込んでみよう。 とはいえ、これには自作したオブジェクトに scikit-learn のインターフェースをダックタイピングで用意するだけで良い。

まずは前処理をするオブジェクトから。 この処理では受け取った値を 2 で割ったものに変換する。 もちろん、この処理自体には特に意味はない。

>>> class MyPreprocessor(object):
...     """前処理 (最低限 fit, transform メソッドが必要)"""
...     def fit(self, X, y):
...         """学習フェーズ"""
...         return self
...     def transform(self, X):
...         """変換フェーズ"""
...         # 2 で割った余りを返す
...         return X % 2
...     def predict(self, X):
...         """分類フェーズ"""
...         # 何もせずに素通しする
...         return X
... 

続いては Pipeline の最後段で推論をするオブジェクト。 このオブジェクトは受け取った値が 0 か非 0 かで True/False を返す処理をする。 こちらも、もちろん処理自体には特に意味はない。

>>> class MyClassifier(object):
...     """回帰・分類処理 (fit メソッドが必要)"""
...     def fit(self, X, y):
...         """学習フェーズ"""
...         return self
...     def predict(self, X):
...         """分類フェーズ"""
...         # X が 0 か非 0 かで True/False を返す
...         return X == 0
... 

上記で用意した自作オブジェクトを組み込んだ Pipeline を作る。 このパイプラインでは値を 2 で割った余りに変換した上で、それが 0 なら True をそうでなければ False を返す。 これはようするに、一連の処理で引数が偶数か奇数かを判定することになる。

>>> steps = [
...     ('pre', MyPreprocessor()),
...     ('clf', MyClassifier()),
... ]
>>> pipeline = Pipeline(steps=steps)

データとしては 0 から 9 までのリストを使う。

>>> import numpy as np
>>> X = np.arange(10)

Pipeline にデータを渡してみよう。 今回に関しては実は fit() 関数は使ってないんだけど、とりあえず機械学習なら普通は使うので呼んでおく。

>>> pipeline.fit(X)
>>> pipeline.predict(X)
array([ True, False,  True, False,  True, False,  True, False,  True,
       False])

うまいこと受け取った引数が偶数か奇数かを判定できている。

めでたしめでたし。