8.3 ハイパーパラメータのチューニング

学習器はデータだけでは決定できないハイパーパラメータを持っています. これは手動で調整するか, データに最も当てはまる形で選択されることが多いです. scikit-learn API の第2の原則「閲覧性」により, クラスの外部からハイパーパラメータを参照し, 変更することは簡単ですが, これも毎回1からプログラムを書くのは面倒です. LogisticRegression に対する LogisticRegressionCV など, 一部の学習器にはハイパーパラメータの調整機能込みのクラスが別に定義されています. さらにここでもパイプラインと同様に, 学習器をラップしてハイパーパラメータ調整機能を追加するものがあります.

まずは, グリッドサーチでハイパーパラメータを決定する GridSearchCV を使う例を紹介します27. この例では, param_grid にロジスティック回帰の正則化パラメータ C の候補を指定して, グリッドサーチで選択しています.

from sklearn.model_selection import GridSearchCV
from sklearn.metrics import make_scorer

GS_logis = GridSearchCV(
    estimator=LogisticRegression(solver='lbfgs', C=1.2),
    param_grid={'C': np.linspace(start=1, stop=10, num=10)},
    cv=5,
    scoring=make_scorer(log_loss, greater_is_better=False)
)

GS_logis.fit(X, y)
## GridSearchCV(cv=5, estimator=LogisticRegression(C=1.2),
##              param_grid={'C': array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])},
##              scoring=make_scorer(log_loss, greater_is_better=False))
GS_logis.predict(X)[:5, ]
## array([0, 0, 0, 1, 0])
GS_logis.best_score_
## -18.996566896429933

当てはまりに応じて決めるため, 「対数損失の小さいもの」という基準で選びます. そのため, scoring=make_scorer(log_loss, greater_is_better=False) という記述が必要です. GridSearchCV もまた, 学習器と同様に .fit(), .predict(), .predict_proba() を持っています. 最終的なスコアを .best_score_ で参照できます. また, 最終的に選ばれたパラメータを .best_params_ を見ることで確認できます. さらにそのパラメータを適用した学習器は .best_estimator_ であるため, 以下の2通りの方法で確認できることになります. 直接 get_params() を指定した場合, 選択前の初期値が返されることに注意してください

# グリッドサーチの結果
GS_logis.best_params_
# GS_logis.best_estimator_.get_params()['C']
# 初期値
# GS_logis.get_params['C']
## {'C': 9.0}

もちろん, これまで紹介したパイプラインとも組み合わせられます.

GS_logis_std_pca = Pipeline([
    ('standardize', StandardScaler()),
    ('PCA', PCA()),
    ('logis+GS', GS_logis)
])
GS_logis_std_pca.fit(X, y)
## Pipeline(steps=[('standardize', StandardScaler()), ('PCA', PCA()),
##                 ('logis+GS',
##                  GridSearchCV(cv=5, estimator=LogisticRegression(C=1.2),
##                               param_grid={'C': array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])},
##                               scoring=make_scorer(log_loss, greater_is_better=False)))])
GS_logis_std_pca.predict(X)[:5, ]
## array([0, 0, 0, 1, 0])

パイプライン内部の各クラスにアクセスするには, .steps または .named_steps を参照します. 前者はタプルのリスト, 後者はディクショナリです. 私はディクショナリのほうが可読性に優れると考えているので後者を使います.

GS_logis_std_pca.named_steps['logis+GS'].best_params_
## {'C': 6.0}

この学習器にはもう1つの書き方があります. 先ほどの例は最後の LogisticRegression のみグリッドサーチに含みましたが, 前処理を連結したパイプラインに対してグリッドサーチを適用することもできます.

GS_in_std_pca_logis = GridSearchCV(
    Pipeline([
    ('standardize', StandardScaler()),
    ('PCA', PCA()),
    ('logis', LogisticRegression(solver='lbfgs', C=1.2))
  ]),
    param_grid={'logis__C': np.linspace(1, 10, 10)}
)
GS_in_std_pca_logis.fit(X, y)
## GridSearchCV(estimator=Pipeline(steps=[('standardize', StandardScaler()),
##                                        ('PCA', PCA()),
##                                        ('logis', LogisticRegression(C=1.2))]),
##              param_grid={'logis__C': array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])})
GS_in_std_pca_logis.predict(X)[:5, ]
## array([0, 0, 0, 1, 0])

ここで, ハイパーパラメータを logis__C と書いていることに注意してください. パイプラインは複数の変換器/学習器を含むので, パラメータ名の衝突を避けるために <部品名>__<元のパラメータ名> に自動でリネームされます. よって, ここでロジスティック回帰の C にアクセスしたいなら 'logis__C' となります.

そしてより大きな違いは書き方ではなく, 処理フローの違いです. 先ほどは前処理の後にグリッドサーチにより何度も反復して計算していたのに対し, 今度は前処理からロジスティック回帰までのフローを1つの学習器としてグリッドサーチで反復しています. つまり前者の書き方では前処理が1回しか行われないのに対し, 後者は反復のたびに処理されます. この違いは単に計算に無駄があるかどうかというだけではありません. 標準化は平均と標準偏差を計算する必要がありますが, これは入力データによって変化します. 全体の平均/標準偏差と 交差検証で分割したデータごとの平均/標準偏差は異なるかもしれません. また, ハイパーパラメータはロジスティック回帰やランダムフォレストなどだけではなく, 前処理にも存在する場合があります. 今回は PCA の主成分の数がそれです. これを固定すべきか, グリッドサーチでの選択の対象にすべきかはケースバイケースです. その意思決定のヒントは後のセクションで言及します.


  1. グリッドサーチや交差検証(CV) は, どうしても計算量が多くなるため多用を避けたいところです. しかしロジスティック回帰に限って言えば, 交差検証の計算量を大きく削減するトリックが考案されています. これは交差検証の計算方法そのものを変えてしまうため, 専用のクラス LogisticRegressionCV として用意されています. ここではAPIの使い方の解説が目的のため, あえて GridSearchCV を使っています.↩︎