Kaggleで学ぶ、Optunaによるハイパーパラメーター自動チューニング僕たちのKaggle挑戦記

Kaggle公式「機械学習」入門/中級講座の次は、本稿で紹介する動画シリーズで学ぶのがオススメ。記事を前中後編に分け、中編ではOptunaを使ったハイパーパラメーターの自動チューニングを試した体験を共有します。

» 2021年12月01日 05時00分 公開
[一色政彦デジタルアドバンテージ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「僕たちのKaggle挑戦記」のインデックス

連載目次

 こんにちは、初心者Kagglerの一色です。毎度、本連載の記事を開いてくれてありがとうございます!

 前回は、「筆者はどうやって『Titanicの次』に学ぶべき応用的な手法を次々と試せたのか」というテーマで、

という記事を公開しました。引き続き同じテーマで、具体的には、

  • 中編(今回):
    • ハイパーパラメーター(ハイパラ)を自動チューニング
  • 後編(次回):
    • モデルのスタッキングや、それに類似のブレンディングという手法を試す

といった体験を共有します。この3本の記事を読めば、機械学習で精度を高めていくための、より現実的なステップについて知ることができると思います。少なくとも次に何を学ぶべきかの参考程度にはなるのではないかと思います。

 詳しくは前回書きましたが、前中後編の3本では、「Titanicの次」という初心者の壁を突き破る方法【第二弾】として、Abhishek Thakur氏によるYouTube動画「Kaggle's 30 Days of ML」を視聴することをオススメしています(関連:【第一段弾】)。動画は6本あり、そのPart-1〜3が前編、Part-4が中編(今回)、Part-5〜6が後編に対応してします。

 筆者は各YouTube動画に1つずつ取り組みんだ結果、Kaggleの公式プログラム“30 Days of ML”参加者限定の特別なコンペティション「30 Days of ML」の成績を着実に向上させることができました。


一色

 ハイパーパラメーターの自動チューニングによって大幅なランクアップを果たしました。機械学習ではチューニングが非常に大切ですね。(一色)


 それではさっそく、ハイパーパラメーター自動チューニングの実体験を紹介していきます。今回も、筆者がYouTube動画を視聴しならがら試した体験内容を、読者の皆さんが追体験するイメージで書いていきます。ターゲットとなる読者は、Kaggle未経験者〜私と同じような初心者Kagglerを想定しています。

 なお本稿のコードは、これまでの連載内容を踏まえた機械学習の実装全体をそのまま掲載しているので長めになっています。注目ポイントを太字にしていますので、太字に着目して他はざっと流し読みすることで、効率的に読んでいただけるとうれしいです。


かわさき

 勉強させていただきます!(かわさき)


3日目(Top 23%):ハイパーパラメーターの自動チューニング

 YouTube動画の4回目では、

  • Optunaを使ったハイパーパラメーターのチューニングHyperparameter tuning using Optuna

を紹介していました。

 Optunaとは、Preferred Networks(PFN、プリファードネットワークス)社が開発した「ハイパーパラメーター自動最適化フレームワーク」のことです。


一色

 名前だけ聞くと何やらすごく難しそうに思えますが、驚くほど簡単に使えます。Optuna以外には、scikit-learnのGridSearchCV(Grid Search:表形式で指定した各種パラメーター候補から最適な組み合わせを探索する、CV:交差検証対応)というハイパーパラメーターのチューニング機能などがありますが、最初からOptunaを使う方が簡単かなと個人的には思っています。



かわさき

 GridSearchCVは本当にちょろっと使ってみましたが、それでも簡単そうだったのに。それよりも簡単ってこと?


 はっきり言って、文章で説明するよりもコードを見る方が早いので、コードから見ていきます。

 前提として前回のリスト1で、交差検証用に5分割(fold:フォールド)したデータセットのtrain_folds.csvファイルを作成済みです。CSVファイルの内容をイメージしやすいように、前回のリスト1を再掲しておきます(リスト1)。

import pandas as pd
from sklearn import model_selection

# 元々のデータセットを読み込んで、[kfold]列を作成
df_train = pd.read_csv('../input/30-days-of-ml/train.csv', index_col=None)
df_train['kfold'] = -1

# 交差検証用にデータセットを5分割(5-fold)してフォールドインデックスを格納
kf = model_selection.KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_indices, valid_indices) in enumerate(kf.split(X=df_train)):
  df_train.loc[valid_indices, 'kfold'] = fold

# 新しいデータセットをCSVファイルに保存
df_train.to_csv('train_folds.csv', index=False)

リスト1 交差検証用に5分割したデータセットを作成してCSVファイルに保存


かわさき

 今さらながらだけど、事前にフォールドを表すインデックスをデータセットに埋め込んでおくと、その後の処理に無駄がない感じがしますね。ボクが「Titanicから始めよう:ベースラインの作成とユーティリティースクリプトの記述」でやっている方法だと、コードを実行するたびに、データセットを分割してから訓練データと検証データを組み立てることになるんだけど、それだと無駄が多そう。


 さらに前提として前回のリスト2では、XGBoostでモデルを作成しました。今回も、そのコードをベースとして使います。

 ただし、前回は5分割したデータセットをfor fold in range(5):というコードでループしながら全てのフォールドを使用しました。今回のハイパーパラメーターのチューニングでは、5分割したうちフォールドインデックス0のフォールドに対してのみ使うことにします。そのため、(前回のリスト2で記述していた)ループしている行(for fold in range(5):)を、def run(trial):という関数に書き換え、fold = 0と固定的に指定します(リスト1)。

import numpy as np
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
from sklearn.metrics import mean_squared_error
from xgboost import XGBRegressor
import optuna # ハイパーパラメーターのチューニングのため

# 5分割済みの訓練&検証データとテストデータのロード
df_train = pd.read_csv('../input/30days-folds/train_folds.csv')
df_test = pd.read_csv('../input/30-days-of-ml/test.csv')

# 利用する特徴量の選択
useful_features = [c for c in df_train.columns if c not in ('id', 'target', 'kfold')]

# カテゴリ変数の選択
categorical_cols = [c for c in useful_features if df_train[c].dtype == 'object']

# 今回は使わないのでカット
# valid_scores = []
# test_predictions = []

def run(trial):
  # print('Trial Number:', trial.number)
  fold = 0

  # 各ハイパーパラメーターの提案値を取得
  learning_rate = trial.suggest_float('learning_rate', 1e-2, 0.25, log=True)
  reg_lambda = trial.suggest_float('reg_lambda', 1e-8, 100.0)
  reg_alpha = trial.suggest_loguniform('reg_alpha', 1e-8, 100.0)
  subsample = trial.suggest_float('subsample', 0.1, 1.0)
  colsample_bytree = trial.suggest_float('colsample_bytree', 0.1, 1.0)
  max_depth = trial.suggest_int('max_depth', 1, 7)

  # フォールドインデックスに一致するものを検証データとして使う
  X_valid = df_train[df_train.kfold == fold].reset_index(drop=True)

  # フォールドインデックスに一致しないものを訓練データとして使う
  X_train =  df_train[df_train.kfold != fold].reset_index(drop=True)

  # テストデータは使わないのでカット

  # 正解値の教師データ
  y_train = X_train.target
  y_valid = X_valid.target

  # 特徴量の選択
  X_train = X_train[useful_features]
  X_valid = X_valid[useful_features]

  # カテゴリー変数の序数エンコーディング
  ordinal_encoder = OrdinalEncoder()
  X_train[categorical_cols] = ordinal_encoder.fit_transform(X_train[categorical_cols])
  X_valid[categorical_cols] = ordinal_encoder.transform(X_valid[categorical_cols])

  # XGBoostのモデルを訓練(fit)する
  model = XGBRegressor(
    #n_jobs=-1, # CPUを使う場合
    tree_method='gpu_hist', gpu_id=-1, predictor='gpu_predictor', # GPUを使う場合
    random_state=42,
    n_estimators=10000, # 早期停止(Early Stopping)するのでチューニングは不要

    # 定義したチューニング対象を各ハイパーパラメーターの引数に指定
    learning_rate=learning_rate,
    reg_lambda=reg_lambda,
    reg_alpha=reg_alpha,
    subsample=subsample,
    colsample_bytree=colsample_bytree,
    max_depth=max_depth)
  model.fit(
    X_train, y_train,
    early_stopping_rounds=300, # 300回精度が向上しなければ早期停止
    eval_set=[(X_valid, y_valid)], # 早期停止するため検証データも指定
    verbose=1000) # 1000個目のestimatorsごとに出力

  # 検証データをモデルに入力して予測する
  preds_valid = model.predict(X_valid)
  # 検証データの評価スコアを取得し、関数の戻り値として返す
  score_valid = mean_squared_error(y_valid, preds_valid, squared=False)
  return score_valid

study = optuna.create_study(direction='minimize')
study.optimize(run, n_trials=5)

リスト2 Optunaを使って最適なハイパーパラメーターを探索するコード

 リスト1では、learning_rate = trial.suggest_float('learning_rate', 1e-2, 0.25, log=True)などというコードで「各ハイパーパラメーターの値を取得」しています。trialは、run()関数の引数に渡されたoptuna.trial._trial.Trial型のオブジェクトです。詳しくは先ほどのリンク先を参照してほしいのですが代表的なものだけ簡単に説明しておくと、このtrialオブジェクトを使って例えば、

  • numberプロパティで、現在の試行番号を取得

したり、

  • suggest_float(name, low, high, *[, step, log])メソッドで、浮動小数点パラメーターの値(例:リスト1のlearning_rate)を
  • suggest_int(name, low, high[, step, log])メソッドで、整数パラメーターの値(例:リスト1のmax_depth)を
  • suggest_categorical(name, choices)メソッドで、カテゴリー変数パラメーターの値(例:活性化関数['sigmoid', 'relu']の選択)を

提案(suggest、候補として提案される値を取得)したりできます。なお、各メソッドの引数である、

  • nameには、ハイパーパラメーターの名前(例:リスト1の'learning_rate')を
  • lowhighには、パラメーター数値候補が取り得る範囲の最小値と最大値を
  • stepには、パラメーター数値候補が取り得る値を離散化する間隔を
  • logには、パラメーター数値候補を対数の定義域からサンプリングする場合はTrue
  • choicesには、候補となるパラメーターカテゴリー値のリスト(例:['sigmoid', 'relu'])を

指定します。ただしsteplogは同時には指定できません。

 上記の各メソッドの戻り値として、候補となるパラメーター値が返されます。例えばsuggest_float()メソッドを呼び出すと、戻り値として0.14938370317232488などの浮動小数点パラメーターの値が返されます。あとはこの値を、ハイパーパラメーターの値として指定するだけです。

 ちなみに、(前々回も名前だけ出したアンサンブル学習の)ブースティング法を用いたXGBoostやLightGBMでは、決定木が先頭から順々に構築されていきます。その仕組み上、XGBoostが構築する決定木の数(=XGBRegressorクラスの引数n_estimators)の決定には、パラメーター値を大きくしたり小さくしたりして調整していくチューニングよりも、性能/精度が向上しなくなった時点で訓練(fit)を打ち切る早期停止Early Stopping)というテクニックを使う方が適切です(脱線しますが、同じ理屈で、ニューラルネットワークで訓練するエポック数の決定にも、増やしたり減らしたりしていくチューニングよりも、早期停止を使う方が適切ですね)。そのため、XGBRegressorクラスの引数n_estimators(デフォルト値:100)には余裕を持って非常に大きな値(ここでは10000)を指定してモデルとなるオブジェクト(model)を生成し、そのmodelfit()メソッドの引数early_stopping_roundsに「何回、精度が向上しないと早期停止するか」の回数と、eval_setに早期停止をするための評価指標となる精度(評価値)の計算用に検証データを指定しています。

 他のrun()関数の中身は、前回と大差がないので説明を割愛します。run()関数の戻り値としては、検証データに対する評価スコア(=最小化したい目的関数の値)を返してください。この評価値に基づき、Optunaがハイパーパラメーターを自動的にチューニングしてくれる仕組みです。

 そのrun()関数を5回呼び出しながら各ハイパーパラメーターを最適化(optimize)するのが、最終行のstudy.optimize(run, n_trials=5)というコードです。5回は少ないので、本来は500回などもう少し多い回数を指定するとよいですが、それだけチューニングに時間がかかることになります。

 変数studyは、optuna.create_study(direction='minimize')関数から戻り値として返されるoptuna.study.study.Study型のオブジェクトで、ハイパーパラメーター最適化の調査(study)を表します。詳しくは先ほどのリンク先を参照してほしいのですが代表的なものだけ簡単に説明しておくと、このstudyオブジェクトを使って例えば先ほどの、

  • optimize(<呼び出す関数名>, n_trials=<試行回数>)メソッドで、目的関数(objective function)の値を返すPython関数を使って最適化

したり、

  • best_paramsプロパティで、最良の性能/精度だった試行(trial)の各パラメーター値を辞書形式で取得

したりできます。リスト3はbest_paramsプロパティの出力例です。

study.best_params
# 出力例:
# {'learning_rate': 0.06717809937233654,
#  'reg_lambda': 3.400765694095951e-06,
#  'reg_alpha': 0.01877635693084812,
#  'subsample': 0.7076113502477361,
#  'colsample_bytree': 0.36318769326066913,
#  'max_depth': 2}

リスト3 最良の性能/精度だった試行の各パラメーター値を取得

 あとは、ここで得た各パラメーター値を使って、前回行ったようなXGBoostの機械学習を行うだけです。蛇足だと思いますが、Pythonの辞書なので、

  study.best_params['learning_rate']

のようなコードで値を取り出せます。

 また、辞書オブジェクト(study.best_params)のキー(=前述のメソッド引数nameで設定したハイパーパラメーター名)と、クラスの各引数のキーワード(=パラメーター名)が一致しているなら、リスト4やリスト5のように辞書オブジェクトごとまとめてセットすることもできます(このように**辞書アンパッキング演算子を関数/メソッドの呼び出しで使う機能はPython 3.5で導入されました)。

from xgboost import XGBRegressor

params = study.best_params

model = XGBRegressor(**params)

print(model)
# 出力例:
# XGBRegressor(
#   base_score=None, booster=None, colsample_bylevel=None,
#   colsample_bynode=None, colsample_bytree=0.23543848635770892,
#   gamma=None, gpu_id=None, importance_type='gain',
#   interaction_constraints=None, learning_rate=0.022627414105588852,
#   max_delta_step=None, max_depth=4, min_child_weight=None,
#   missing=nan, monotone_constraints=None, n_estimators=100,
#   n_jobs=None, num_parallel_tree=None, random_state=None,
#   reg_alpha=0.0027877129949233346, reg_lambda=2.5247375274846955,
#   scale_pos_weight=None, subsample=0.5203998004593435,
#   tree_method=None, validate_parameters=None, verbosity=None)

リスト4 XGBoostでまとめてパラメーターをセットする例

from sklearn.ensemble import RandomForestRegressor

params = study.best_params

model = RandomForestRegressor(**params)

print(model)
# 出力例:
# RandomForestRegressor(min_samples_split=18, n_estimators=174)

リスト5 ランダムフォレストでまとめてパラメーターをセットする例


一色

 やっぱりOptunaは簡単で便利すぎますね。使わない選択肢はないと思います。



かわさき

 上のコードを見ながら自分で一度コードを書いてみれば、だいたいのやり方は把握できそうです。これで適切なハイパーパラメーターが割り出せるとは……!



 以上、今回の中編では、YouTube動画の4回目で体験したことを説明しました。ハイパーパラメーターのチューニングはKaggleコンペや機械学習では外せないスキルだと思います。本稿がそのスキル習得のヒントになっているとうれしいです。

 次回の後編では、YouTube動画の5回目〜6回目で体験したこととして、モデルのブレンディングとスタッキングを説明します。これらも非常に有用なスキルですのでオススメです。お楽しみに。

「僕たちのKaggle挑戦記」のインデックス

僕たちのKaggle挑戦記

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。