まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

sklearn.pipelineを試す

sklearn.pipelineとは

sklearn.pipeline とは、前処理用のScaler(変換器)や機械学習モデルを一括で処理するためのオブジェクトを生成する。

これをおこなうことで、管理が容易になったり処理コード部分を簡潔に書くことができる。

実装

データはボストン住宅価格を使用する。

from sklearn.datasets import load_boston

dsn = load_boston()

X = pd.DataFrame(dsn.data, columns=dsn.feature_names)

y = pd.DataFrame(dsn.target, columns=['target'])

f:id:chito_ng:20200602091154p:plain:w500

Scaler

Scalerは以下の2つを用いる

StandardScaler

StandardScalerはデータの標準化をおこなう。

scikit-learn.org

qiita.com

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit_transform(X)

このとき、返り値はArrayとなる。

f:id:chito_ng:20200602091706p:plain:w500

自作Scaler

以下で自作のScalerを作成する。自作Scalerという概念の詳細はそのうち記事に書くので省略。

ここで作成するAddCrim2は特徴量CRIMの値を引数value倍(初期値は2)したCRIM2という特徴量を追加するシンプルな内容(例示用なのでこの処理自体に意味はないです)。 

from sklearn.base import BaseEstimator, TransformerMixin

class AddCrim2(BaseEstimator, TransformerMixin):
    def __init__(self,value=2):
        self.value = value

    def fit(self, X, y=None):
        return self

    def transform(self, X):
         # 直接の書き換えが起きないようにcopy
        _X = X.copy()
        
        # 新たな特徴量の作成
        _X['CRIM2'] = _X['CRIM'] * self.value
        return _X

tranceformer = AddCrim2(value=3)

tranceformer.fit_transform(X)

f:id:chito_ng:20200602092402p:plain

pipeline

ここから本題のpiplineについて。

処理としては、

  1. 前述の自作Scaler AddCrim2で変換をおこなう
  2. 前述のsklearnのScalerStandardScalerで変換をおこなう
  3. 機械学習モデル LinearRegressionで線形回帰をおこなう

といった流れとなる。

piplineを使わないと以下のような処理となる。

from sklearn.linear_model import LinearRegression

# AddCrim2でXを変換(特徴量追加)
scaler1 = AddCrim2(value=4)
X2 = scaler1.fit_transform(X)

# StandardScalerでX2,yを変換
scaler2 = StandardScaler()
X3 = scaler2.fit_transform(X2)
y3 = scaler2.fit_transform(y)

# LinearRegressionをX3, y3で学習
model = LinearRegression()
model.fit(X3, y3)

# LinearRegressionの学習結果をX3,yで評価
model.score(X3, y3)
# => 0.7406426641094093

pipelineを使用する場合、任意の処理名と処理クラスのセットのタプルを各処理の順番で渡す。ちなみに辞書ではないのは、明確に処理に順番があるため。
また、各処理の引数はset_params関数を用いて、引数を 処理名__引数名で指定することができる(_は2つなので注意)。

pipelineオブジェクトが作成できたらfitでデータを渡す。

from sklearn.pipeline import Pipeline

add_scale_ols_pipeline = Pipeline([
    ('add', AddCrim2()),
    ('scaler', StandardScaler()),
    ('ols', LinearRegression())
])

add_scale_ols_pipeline.set_params(add__value=4) # AddCrim2の引数valueを4に設定
add_scale_ols_pipeline.fit(X, y)

なお、pipelineオブジェクトを作成した際に、各処理の引数がどう設定されているかが表示されるので引数指定の際に参考になる。

f:id:chito_ng:20200602094510p:plain:w500

このpipelineオブジェクトでの変換・学習結果の評価もおこなえる。

scale_ols_add_pipeline.score(X, y)
# =>0.7406426641094095

pipelineの注意点

各処理で設定する引数

AddCrim2のコードは特徴量に関するコードなのでXだけを処理する。しかし、引数としてy = Noneとわざわざ設定している理由について。

先程のpipelineコードではLinearRegression用にfit時にX, yを渡している。そのため、各fit処理でX, yが引数として渡される。 そのため、AddCrim2でのfitでも共通してX, yともに渡す必要があるので、引数yへの処理が記載されていないと引数が多くてエラーとなる。そのため、yについての引数についても書きつつ、使用自体はしないのでy = Noneと指示している。

# オリジナル
class AddCrim2(BaseEstimator, TransformerMixin):
    def __init__(self,value=2):
        self.value = value

    def fit(self, X, y=None):
        return self

    def transform(self, X):
         # 直接の書き換えが起きないようにcopy
        _X = X.copy()
        
        # 新たな特徴量の作成
        _X['CRIM2'] = _X['CRIM'] * self.value
        return _X

# 引数1つ
class AddCrim2_2(BaseEstimator, TransformerMixin):
    def __init__(self,value=2):
        self.value = value

    def fit(self, X):
        return self

    def transform(self, X): # yへの引数なし
         # 直接の書き換えが起きないようにcopy
        _X = X.copy()
        
        # 新たな特徴量の作成
        _X['CRIM2'] = _X['CRIM'] * self.value
        return _X


scale_ols_add_pipeline = Pipeline([
    ('add', AddCrim2_2()), # 引数1つ
    ('scaler', StandardScaler()),
    ('ols', LinearRegression())
])

scale_ols_add_pipeline.set_params(add__value=4)
scale_ols_add_pipeline.fit(X,y) # AddCrim2_2, StandardScaler, LinearRegressionそれぞれのfitが走る。AddCrim2_2のfitはfit(X, y)として走るがfit(X)と定義されているので引数エラーとなる

f:id:chito_ng:20200602095745p:plain

各処理の順番

前述のように、指定された順番で処理が走る。つまり、前工程で処理したデータを次工程に渡す。このとき、前工程でどのような形で返ってくるか、次工程でどのような形でデータを受け取るかを意識しておく必要がある。

今回の例示コードのAddCrim2は、前述のように データフレームを受け取って処理する 。また、StandardScalerは受け取る形はデータフレームでもArrayでも良いが 処理結果はArrayで返ってくる
そのため、処理順がAddCrim2StandardScalerは良いが、逆の StandardScalerAddCrim2 ではエラーが起きる(仮にエラーが出なくても、標準化してから特徴量を掛け算して加えるという処理はわけがわからないので例示用として考えてください)。

scale_ols_add_pipeline = Pipeline([
    ('scaler', StandardScaler()), # 順番入れ替える
    ('add', AddCrim2()),
    ('ols', LinearRegression())
])

scale_ols_add_pipeline.set_params(add__value=4)
scale_ols_add_pipeline.fit(X,y)

f:id:chito_ng:20200602100436p:plain

参考

pipelineについてよくまとまっている、また、pipelineの簡素版であるmake_pipelineの紹介もおこなっている。

zerofromlight.com