連載
» 2021年12月01日 05時00分 公開

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

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

[一色政彦,デジタルアドバンテージ]

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

「僕たちの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などの浮動小数点パラメーターの値が返されます。あとはこの値を、ハイパーパラメーターの値として指定するだけです。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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