🌟

scikit-learn の機械学習パイプライン

4 min read

はじめに

機械学習で予測モデルを作るときは

  1. データの分割
  2. データの前処理
  3. 予測モデルの学習
  4. クロスバリデーションによるハイパーパラメータチューニング

といった手順を踏む必要がある。慣れるまではこれらの手順に対応する scikit-learn のクラスをひとつひとつ呼び出して自分で一連の処理をやってみるのが勉強になるが、慣れてしまうと似たような手続きを毎回書くのは非常に面倒くさい。

scikit-learn には、この一連の処理を簡潔に記述するためのパイプラインの仕組みがあるので、その使用方法について説明する。

一連のコードは Google Colab 上にアップロードしてある。

https://colab.research.google.com/drive/1PiAXUqcZiCXacJLjhexJX5yC5vVj1uM9?usp=sharing

データの分割

これは人間が管理すべき問題なので、自動化もやろうと思えばできるだろうが、人間がいちいちやったほうがよい。機械学習をやるとき、データは基本的に

  1. 訓練データ
    • 教師データともいう。予測モデルを学習させるためのデータ
  2. 検証データ
    • 予測モデルが未知のデータにどれくらい当てはまるかを検証するためのデータ
  3. テストデータ
    • 作成した予測モデルが実際の運用時にどれくらいの性能となるか評価するためのデータ

の 3 つに分割することになる。検証データへの当てはまりを見てモデルをチューニングしてもよいが、テストデータへの当てはまりを見てモデルをチューニングするのはズルである[1]

データの分割は sklearn.model_selection.train_test_split で行う。検証データはクロスバリデーションのときに訓練データから一部を取り出して作るので、最初は訓練データとテストデータに分割しておけばよい。

データをシャッフルした上で分割するので、分割後のデータが元のデータの何行目のデータなのかを管理するのが面倒になる。pandas.DataFrame や pandas.Series の状態で保持しておけばインデックスが維持されるので、特別な理由がなければ pandas.DataFrame に変換しておくほうがよい[2]

# 疑似データの生成(ここは適宜データの読み込みに書き換える)
import numpy as np
import pandas as pd

from sklearn.datasets import make_regression

# numpy.ndarray 形式
X_raw, y_raw = \
    make_regression(n_samples=300, n_features=50, n_informative=30, random_state=42)

# pandas.DataFrame 形式に変換
X = pd.DataFrame(X_raw)
y = pd.DataFrame(y_raw)
# データの分割
from sklearn.model_selection import train_test_split

# 訓練データ、テストデータの分割
X_train, X_test, y_train, y_test = \
    train_test_split(X, y, test_size=0.25, random_state=42)

前処理と予測のパイプライン

読み込んだデータの加工 → モデルのフィッティング までの一連の処理をひとまとめにする仕組みが sklearn.pipeline.Pipeline である。たとえば StandardScaler で前処理をしたあとで、Ridge による回帰を行う場合には以下のようなコードを書く。

# パイプライン
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

pipe = Pipeline([
            ('scaler', StandardScaler()),
            ('regressor', Ridge()),
        ])

Pipeline の引数は steps であり、内部的には named_steps として保持されている。ここで各ステップにつけた名前は、ハイパーパラメータチューニングの際に使う。

print(pipe.named_steps)
output
{'scaler': StandardScaler(copy=True, with_mean=True, with_std=True), 'regressor': Ridge(alpha=1.0, copy_X=True, fit_intercept=True, max_iter=None,
      normalize=False, random_state=None, solver='auto', tol=0.001)}

クロスバリデーションとハイパーパラメータの探索

GridSearchCV を使うことで、クロスバリデーションによって検証データへの当てはまり(R2CVスコア)を改善するようにハイパーパラメータの探索を行うことができる。

グリッドサーチとは、空間を格子状に分割し、その格子点をすべて調べることでその中からもっともよい格子点を選ぶ方法である。探索したい空間が2次元(つまりハイパーパラメータが2個)までならワークするが、各次元における格子点の数をn、次元をdとするとn ^ d個の点を探索することになるので、3次元以上だと非常に効率が悪くなる(たとえばn=100として3次元なら 1,000,000 回もモデルを学習させて探すことになる)。

グリッドサーチは実装が簡単なため第一の選択肢に上がるが、格子点上しか探せないため精度にも欠ける。より精密にチューニングしたい場合や、ハイパーパラメータの個数が多い場合には工夫が必要になる[3]

GridSearchCVfit でハイパーパラメータのチューニングまで行い、predict でもっともよかったハイパーパラメータの予測器で予測を行ってくれる。

# クロスバリデーションとグリッドサーチによるハイパーパラメータ探索
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import r2_score
from sklearn.metrics.scorer import make_scorer

# ハイパーパラメータの探索範囲の指定
# 「ステップ名__ハイパーパラメータ名」の形式で指定する
params = {
    'regressor__alpha': np.logspace(-8, -1, 100),
}

# 評価関数を指定する場合は make_scorer で定義する
scorer = make_scorer(r2_score, greater_is_better=True)

# グリッドサーチの設定
## scoring ... 評価関数を指定する場合はここに書く
## cv ... k-fold CV の k の数。訓練データを何分割するか
## refit ... 最後に一番よかったハイパーパラメータの組み合わせで、すべての訓練データを用いて学習し直す
## n_jobs ... 並列実行の job 数。 -1 を指定すると使用可能なプロセッサをすべて使う
## verbose ... 進捗状況の表示。1,2,3 が指定可能で、数が増えるほど細かい情報が表示される
model = GridSearchCV(pipe, params, scoring=scorer, cv=5, refit=True, n_jobs=-1, verbose=1)
# 学習とハイパーパラメータの探索
model.fit(X_train, y_train.values.flatten())
# 全組み合わせに対するスコアの情報
result = pd.DataFrame.from_dict(model.cv_results_)

# もっともよかったときのR2CVスコア
model.best_score_

# もっともよかったハイパーパラメータ
model.best_params_

# もっともよかったハイパーパラメータで学習した予測器
model.best_estimator_

# テストデータへの当てはまりを確認
from sklearn.metrics import r2_score

r2_score(y_test, model.predict(X_test))
脚注
  1. ガチの不正である。コンテストでやったら出禁になるし、ビジネスでやったらクビが飛ぶ。 ↩︎

  2. インデックスの情報は scikit-learn を使う限り前処理などどこかの段階で結局失われてしまうので、あまりこだわらなくてもよい。sklearn-pandasというライブラリを使うことで多少は保持していられるが、クセがあるのでここでは使わない。 ↩︎

  3. Optuna などのハイパーパラメータ自動最適化フレームワークを用いるとよい。 ↩︎