Pipelineを用いた最適化


AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた Day40


経緯についてはこちらをご参照ください。



■本日の進捗

●make_pipelineを理解

■はじめに

引き続き「Pythonではじめる機械学習(オライリー・ジャパン)」で学んでいきます。

前回に引き続きpipelineについてですが、より真価を発揮したpipelineの世界を覗いていきます。

■TransformerとEstimatorの連結

前回は標準化とロジスティック回帰をステップとしてPipeloneに連結してもらいました。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_predict
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, roc_curve, roc_auc_score
from sklearn.pipeline import Pipeline

n_samples = 500
n_features = 30

X, y = make_classification(n_samples=n_samples, n_features=n_features, 
                           n_informative=5, n_redundant=10, n_repeated=5,
                           n_clusters_per_class=1, class_sep=2, flip_y=0.1, 
                           random_state=8)

feature_names = [f"Feature{i}" for i in range(1, n_features+1)]

df = pd.DataFrame(X, columns=feature_names)
df['target'] = y

model = LogisticRegression(random_state=8)

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=8)
pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('classifier', LogisticRegression())])
pipeline.fit(X_train, y_train)
predict = pipeline.predict(X_test)
accuracy = pipeline.score(X_test, y_test)
print('score with pipeline class = {}'.format(accuracy))

ここでは2つを連結していますが、特徴量抽出、特徴量選択、スケール変換、機械学習モデルといった任意数のアルゴリズムを連結することが可能です。

Pipelineに乗せるステップには主にTransformerとEstimatorの2つがあります。

Transformer
transformメソッドを持つもの。データ変換などの前処理を基本とします。

Estimator
fitメソッドを持つもの。モデルを学習させるためのアルゴリズムのことです。

各ステップには、__(連続したアンダースコア)を除いた任意の名前を付ける必要があり、上記の例ではStandardScalerのインスタンスにscaler、LogisticRegressionのインスタンスにclassifierという名前を付けてPipelineに渡しています。

実はこの工程も簡略化できる便利な関数がscikit-learnには用意されています。

■make_pipeline関数

前述の通り、make_pipelineを用いればステップの名前も自動で付けてくれます。

先ほどのコードをmake_pipelineに書き換えてみます。(とは言っても名前を付ける部分以外はほとんど変わりません)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_predict
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, roc_curve, roc_auc_score
from sklearn.pipeline import make_pipeline

n_samples = 500
n_features = 30

X, y = make_classification(n_samples=n_samples, n_features=n_features, 
                           n_informative=5, n_redundant=10, n_repeated=5,
                           n_clusters_per_class=1, class_sep=2, flip_y=0.1, 
                           random_state=8)

feature_names = [f"Feature{i}" for i in range(1, n_features+1)]

df = pd.DataFrame(X, columns=feature_names)
df['target'] = y

model = LogisticRegression(random_state=8)

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=8)
pipeline = make_pipeline(StandardScaler(),
                         LogisticRegression())
pipeline.fit(X_train, y_train)
predict = pipeline.predict(X_test)
accuracy = pipeline.score(X_test, y_test)
print('score with make_pipeline = {}'.format(accuracy))

内部的にはクラス名を小文字化した名前(StandardScalerの場合はstandardscaler)が自動で与えられ、複数ある場合はハイフン+1からの数字が与えられます(standardscaler-1, standardscaler-2…といった感じ)。

PipelineのLogisticRegressionステップから学習した係数を抜き出してみます。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_predict
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, roc_curve, roc_auc_score
from sklearn.pipeline import make_pipeline

n_samples = 500
n_features = 30

X, y = make_classification(n_samples=n_samples, n_features=n_features, 
                           n_informative=5, n_redundant=10, n_repeated=5,
                           n_clusters_per_class=1, class_sep=2, flip_y=0.1, 
                           random_state=8)

feature_names = [f"Feature{i}" for i in range(1, n_features+1)]

df = pd.DataFrame(X, columns=feature_names)
df['target'] = y
model = LogisticRegression(random_state=8)

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=8)
pipeline = make_pipeline(StandardScaler(),
                         LogisticRegression())
pipeline.fit(X_train, y_train)
predict = pipeline.predict(X_test)
accuracy = pipeline.score(X_test, y_test)

classifier = pipeline.named_steps["logisticregression"]
print('logisticregression.shape = {}'.format(classifier.coef_))



■Pipeline+GridSearchCV

前回も言及したように、Pipelineが一番真価を発揮するのはデータの取り扱いに細心の注意が必要でその回数が多い交差検証を用いたグリッドサーチ(交差検証)と併用する時ではないでしょうか。

最後に正しくデータを取り扱った場合の交差検証を実施してみたいと思います。今回はロジスティック回帰のハイパーパラメータ、CとPenalty(L1正規化とL2正規化の混合型であるelasticnetをl1_ratioで調整)を最適化してみます。

import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

n_samples = 500
n_features = 30

X, y = make_classification(n_samples=n_samples, n_features=n_features, 
                           n_informative=5, n_redundant=10, n_repeated=5,
                           n_clusters_per_class=1, class_sep=2, flip_y=0.1, 
                           random_state=8)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=8)

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', LogisticRegression(solver='saga', penalty='elasticnet', random_state=8, max_iter=10000))
])

param_grid = {
    'model__C': [0.01, 0.1, 1.0, 10, 100],
    'model__l1_ratio': [0.1, 0.5, 0.7, 0.9]
}

grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', verbose=1)
grid_search.fit(X_train, y_train)

print("Best parameters : ", grid_search.best_params_)

best_score = grid_search.best_score_
print("grid_search.best_score_: {:.2f}".format(best_score))

test_accuracy = grid_search.best_estimator_.score(X_test, y_test)
print("Test accuracy: {:.2f}".format(test_accuracy))

Penaltyにelasticnetを用いる場合は、Solverはsagaが必須になります。

グリッドサーチ中に交差検証の内側における訓練データで学習したモデルが検証データを用いて出した最高のスコアはbest_score_で、グリッドサーチの結果として得られる最良のパラメータで学習したモデルはbest_estimator_でそれぞれ取得することができます。

また、best_estimator_のスコアを別途算出するためにtrain_test_splitをここでも用いています。もちろんこのデータを交差検証内に事前にリークさせてはいけないし、させないために予め分離しています。あくまで学習においての訓練データと検証データはPipelineがリークしないよう適切に扱ってくれています。

■おわりに

注意深く読んでいただいた方はお気付きかもしれませんが、最終的なテストスコアはデフォルトパラメータと同等ですが、交差検証の内側におけるベストスコアはパラメータ最適化をする前より下回っています。

これはパラメータグリッドの範囲や区切り方が良くなかったのかもしれないし、データの選び方(交差検証のk分割数や乱数性)がたまたま良くなかったのかもしれないし、過剰適合をしてないからこそなのかもしれません。そしてそもそもペナルティの与え方が適切ではないのかもしれません。

いくらPipelineを噛ませた交差検証をグリッドサーチにラップさせたからといって、記述の範囲外のことまで検証してはくれません。

本当の最適解や最適なモデルは交差検証の外にあるのかもしれないし、まだこの世にないアルゴリズムなのかもしれません。

まるで人生のように奥深いですね。いや、人生だとそんなに深くなかったか?
機械学習モデルに負けないくらい奥深い一生にしていきましょう…

■参考文献

  1. Andreas C. Muller, Sarah Guido. Pythonではじめる機械学習. 中田 秀基 訳. オライリー・ジャパン. 2017. 392p.
  2. ChatGPT. 4o mini. OpenAI. 2024. https://chatgpt.com/
  3. API Reference. scikit-learn.org. https://scikit-learn.org/stable/api/index.html


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です