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))
score with make_pipeline = 0.99
内部的にはクラス名を小文字化した名前(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_))
classifier.coef_ = [[ 0.30708587 0.10407717 0.37740424 0.25880891 0.03791796 0.09219188
0.09219188 0.10407717 -0.37190277 0.29821942 -0.01768578 -0.00888112
-0.62009413 0.09219188 -0.56744797 0.0015683 -0.12855008 0.29821942
-0.32335506 -0.55687832 -0.53196704 0.43224806 0.30708587 -0.02841978
-0.20858333 -0.12549382 -0.3238781 -0.97680471 -0.03216202 0.37166722]]
■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))
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Best parameters : {'model__C': 0.01, 'model__l1_ratio': 0.5}
grid_search.best_score_: 0.95
Test accuracy: 0.99
Penaltyにelasticnetを用いる場合は、Solverはsagaが必須になります。
グリッドサーチ中に交差検証の内側における訓練データで学習したモデルが検証データを用いて出した最高のスコアはbest_score_で、グリッドサーチの結果として得られる最良のパラメータで学習したモデルはbest_estimator_でそれぞれ取得することができます。
また、best_estimator_のスコアを別途算出するためにtrain_test_splitをここでも用いています。もちろんこのデータを交差検証内に事前にリークさせてはいけないし、させないために予め分離しています。あくまで学習においての訓練データと検証データはPipelineがリークしないよう適切に扱ってくれています。
■おわりに
注意深く読んでいただいた方はお気付きかもしれませんが、最終的なテストスコアはデフォルトパラメータと同等ですが、交差検証の内側におけるベストスコアはパラメータ最適化をする前より下回っています。
これはパラメータグリッドの範囲や区切り方が良くなかったのかもしれないし、データの選び方(交差検証のk分割数や乱数性)がたまたま良くなかったのかもしれないし、過剰適合をしてないからこそなのかもしれません。そしてそもそもペナルティの与え方が適切ではないのかもしれません。
いくらPipelineを噛ませた交差検証をグリッドサーチにラップさせたからといって、記述の範囲外のことまで検証してはくれません。
本当の最適解や最適なモデルは交差検証の外にあるのかもしれないし、まだこの世にないアルゴリズムなのかもしれません。
まるで人生のように奥深いですね。いや、人生だとそんなに深くなかったか?
機械学習モデルに負けないくらい奥深い一生にしていきましょう…
■参考文献
- Andreas C. Muller, Sarah Guido. Pythonではじめる機械学習. 中田 秀基 訳. オライリー・ジャパン. 2017. 392p.
- ChatGPT. 4o mini. OpenAI. 2024. https://chatgpt.com/
- API Reference. scikit-learn.org. https://scikit-learn.org/stable/api/index.html