パイプラインパンチ


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


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



■本日の進捗

●Pipelineを理解

■はじめに

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

今回はデータを正しく扱うための注意点と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

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

# 本稿では言及しないがちょっとまずい記述なので注意
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = LogisticRegression(random_state=8)

y_prob = cross_val_predict(model, X_scaled, y, cv=5, method='predict_proba')

roc_auc = roc_auc_score(y, y_prob[:, 1])

print('AUC = {:.2f}'.format(roc_auc))

ここでコメントアウトで注意書きをしているのはスケール変換を行っている部分です。何がまずいのかと言うと、スケール変換したデータを交差検証に渡しているので、交差検証がテストデータとして抜き出したデータもすべてスケール変換済みデータとなってしまうからです。

これがMinMaxScalerだとより分かりやすく、テストデータがデータセット全体における最大最小値を基準に調整された値ということになってしまうのです。実際の入力データはそれより大きかったり小さかったりする可能性もあり、その時にこの場合の汎化性能と同じ性能が出るとは限りません。

これも「データのリーク」のひとつで、テストデータは学習モデルにとって”全くの初見”である必要があるので、前処理などのどんな形であれ学習に関与してはいけないのです。

この場合、正しく行うのであれば、まずはテストデータと交差検証に用いるデータを分離してから残りのデータを交差検証に渡し、交差検証の内側で訓練データとなるデータに対してのみスケール変換を行うべきなのです。

■Pipeline

scikit-learnにはこの手順を簡単に正しい順番で一連の作業をひとつのオブジェクトとしてまとめてくれるPipelineクラスというものが存在します。

Pipelineとは、データの前処理からモデルの訓練、評価などをひとつにまとめることで、簡潔に記述できデータの取り扱いについても順序良くリークのない形で実行してくれるクラスです。

データの管理が不要な分、特徴量ごとに異なる前処理を行う場合にはグリッド用パラメータが煩雑になったり、処理の理解が難しく(ブラックボックス化)なったりするので注意が必要です。

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 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

# ここではスケール変換を行わない
# scaler = StandardScaler()
# X_scaled = scaler.fit_transform(X)

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))

まるで新しいモデルを構築したかのようです。

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 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

# ここではスケール変換を行わない
# scaler = StandardScaler()
# X_scaled = scaler.fit_transform(X)

model = LogisticRegression(random_state=8)

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('classifier', LogisticRegression())])
pipeline.fit(X_train, y_train)
y_prob = cross_val_predict(pipeline, X, y, cv=5, method='predict_proba')
roc_auc = roc_auc_score(y, y_prob[:, 1])
print('AUC with pipeline cv = {:.2f}'.format(roc_auc))

なんということでしょう。僅か3行で学習が完了しています。しかもデータの取り扱いを注意することなく、です。

■おわりに

今回の前処理に関する注意事項は、本当なら機械学習を学ぶ初日に理解しておかなければいけないことだったのかもしれません。Pipelineクラスは確かに便利ですが、本来あるべき学習の流れを忘れないように、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


コメントを残す

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