Optunaを使ってXGBoostのハイパーパラメータチューニングをやってみる
こんばんは、新米データサイエンティスト(@algon_fx)です。今月からジムも再開して、しっかり走り込んでいます。やっぱり人間、定期的なエクササイズは絶対に必要ですね。
さて、日本で機械学習に携わっている方であれば、12月初旬のビッグニュースを耳にした方も多いと思います。そうです!PFNさんからハイパーパラメータ最適化ツール「Optuna」が公開されました!
まだベータ版とのことですが、データ分析を生業として生きている新米としては気になってしょうがない訳です(笑)
ってことで本日の記事は速報版として、リリースされたばかりのOptunaを使ってXGBoostのハイパーパラメータチューニングを行ってみました。使ってみた感想として・・もう素晴らしいの一言です!シンプルかつ直感的に操作できる点が特に素晴らしい!
機械学習の初心者の方向けの記事です。Optunaの使い方だけ見たいって方は、前半の解説は読み飛ばしてください。
ハイパーパラメータとは
そもそも機械学習でいうハイパーパラメータってなんなの?って話からです。世間では機械学習=人工知能的な文脈で扱われており、機械学習って大量のデータを勝手に機械が学習してくれるんでしょ?的な考えを持っている方も少なくないです。
これは大きな間違いです。
機械学習のほとんどの手法には「人間が決めなくてはいけない設定」があります。この設定がハイパーパラメータと呼ばれているのです。

ハイパーパラメータによりアルゴリズムの挙動や手法の細かい動作などを設定することが可能です。機械学習手法に毎に設定しなくてはいけないハイパーパラメータ の項目数は異なります。
シンプルな構造の手法であれば数個の設定で良いですし、最近流行りの深層学習やXGBoostともなるとハイパーパラメータの数はとても多いです。
ハイパーパラメータチューニングとは
ハイパーパラーメータに正解の値というのはありません。扱うデータセットの特性や、推測したいタスクなどにより細かく調整をしなくてはいけません。
この調整がハイパーパラメータチューニングと呼ばれるものです。機械学習の現場ではハイパーパラメータの値を試行錯誤を繰り返しながら調整します。
ハイパーパラメータの調整には様々な手法があります。ここではメジャーな2種類を紹介します。
グリッドサーチ
おそらく最もメジャーで使ったことがある方も多いと思います。グリッドサーチでは、検証をしたいハイパーパラメータの候補を用意して、それらの全ての組み合わせを試す方法です。例えば調整したいパラメータが2つ、それぞれ5個の候補があるとしたら、全ての組み合わせのモデルを構築するので合計25回のモデル訓練を行います。想像しやすいですが、計算コストが非常に高いです。大規模なデータかつ多数のパラメータの検証を行うのには適していません。
ベイズ最適化
上のグリッドサーチは全ての組み合わせを検証するので確実性はありますが使いづらい反面もあります。そこで、効率的に調整を行える手法としてベイズ最適化という選択肢があります。ベイズ最適化では、過去の試行結果から次に試す値を確率分布と関数に基づいて決めます。グリッドサーチは総当たりで検証するのに対し、ベイズ最適化はアルゴリズムにより最適すべきパラメータを探しながら調整を行なってくれるのです。
本記事の主題でもある「optuna」は、まさにベイズ最適化アルゴリズムの一種を利用したハイパーパラメータチューニングのフレームワークです!
そこで本記事ではグリッドサーチを手軽に行えるScikit-learnのGridSearchCVとOptunaを使って簡単な比較を行なってみます。
あらかじめ明記しますが、比較と言っても両手法の優劣をつけたい訳ではありません。両方ともメリット・デメリットがあります。比較と言っても、単純に同じデータと手法でチューニングをやってみようって話です。
本記事でやること
メインはOptunaの紹介ですが、簡単に扱うデータと推測するタスクを紹介します。
扱うデータはOANDA FX APIから取得したドル円の10分足データです。終値、始値、高値、安値が含まれています。(参考:OANDA APIの基本技10選)
このデータを簡単な前処理を行い、10分後のドル円レートが今の終値から上昇するか下降するかを推測するタスクを行います。上昇すれば「1」、下降したら「0」と言った具合の分類問題です。モデリングはXGBoostを使います。(参照:XGBoostでFX予測)
下の図は日足ですが、これが10分足になったイメージです。今の相場データから10分後の相場を推測する訳です。ご覧いただいてお判り頂けると思いますが、このモデルを使っても100%利益は出ません。解りやすいように極端に簡素化しています。
詳しくは「FXトレードでロジスティック回帰を独自テクニカル指標として活用する方法」をご参照ください。前処理などを詳しく解説しています。
推測するターゲットや特徴量をしっかり作り込めば、実際のトレードでも役に立ちます。本記事はあくまで初心者向けの実装方法チュートリアルですので悪しからず。
では、やって見ましょう!
STEP1 データの前処理
まずはデータの簡単をやっちゃいましょう。ご自身の環境で実装してみたい方は下記のリンクからCSVファイルをダウンロードしてください。
【使うCSVファイル】
usd_10min_api.csv
必要なライブラリをインポートします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# データ解析のライブラリ import pandas as pd import numpy as np # Scikit-learn from sklearn.model_selection import train_test_split from sklearn.metrics import confusion_matrix, accuracy_score from sklearn.model_selection import cross_val_score, GridSearchCV # XGBoost import xgboost as xgb from xgboost import XGBClassifier |
主役のOptunaは後ほどインストールを行なってインポートします。またグリッドーサーチの実装はScikit-learnのGridSearchCVを使います。おそらく最も一般的な実装法です。
では、ドル円の10分足データを読み込みましょう。
1 2 3 4 5 6 7 |
# CSVファイルの読み込み df = pd.read_csv("usd_10min_api.csv") # 最後の5行を表示 df.tail() |
1 2 3 4 5 6 7 8 9 |
--出力 Unnamed: 0 time close open high low volume 16417 16417 2018/08/10 15:30:00 111.038 110.936 111.050 110.936 453 16418 16418 2018/08/10 15:40:00 111.052 111.035 111.150 111.035 385 16419 16419 2018/08/10 15:50:00 111.013 111.054 111.060 110.991 376 16420 16420 2018/08/10 16:00:00 110.915 111.012 111.018 110.909 492 16421 16421 2018/08/10 16:10:00 110.773 110.913 110.927 110.692 880 |
OANDA APIから取得したデータです。
ターゲットラベルの作成やら、訓練/テストデータへの分割やらの細かい作業を一気にやってしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# 終値を1日ずらして差分を計算 df['close+1'] = df.close.shift(-1) df['diff'] = df['close+1'] - df['close'] df = df[:-1] # 上昇したら「1」、下降したら「0」へデータを変換 mask1 = df['diff'] > 0 mask2 = df['diff'] < 0 column_name = 'diff' df.loc[mask1, column_name] = 1 df.loc[mask2, column_name] = 0 # 不要なカラムを落とす df.rename(columns={"diff" : "target"}, inplace=True) del df['time'] del df['close+1'] df = df[['target', 'close', 'open', 'high', 'low', 'volume']] # データフレームの行数と列数を取得 n = df.shape[0] p = df.shape[1] # データフレームからNumpy配列へ変換 data = df.values # 訓練データ8割、テストデータ2割へ分割 train_start = 0 train_end = int(np.floor(0.8*n)) test_start = train_end + 1 test_end = n data_train = data[np.arange(train_start, train_end), :] data_test = data[np.arange(test_start, test_end), :] # 特徴量(X)とターゲット(y)へ切り分け X_train = data_train[:, 1:] y_train = data_train[:, 0] X_test = data_test[:, 1:] y_test = data_test[:, 0] print(X_train.shape) print(X_test.shape) |
1 2 3 4 5 |
--出力 (13136, 5) (3284, 5) |
詳細はコードを見てください。とてもシンプルな処理です。単純に10分後の終値と現在の終値の差分を計算して、上がっていれば1、下がっていたら0をターゲットして追加したのみです。
STEP2 グリッドサーチでXGBoostを調整
まずはグリッドサーチでXGBoostのハイパーパラメータをチューニングしてみましょう。今回調整するハイパーパラメータは以下の通りです。
n_estimators:決定木の本数
max_depth:各決定木の最大層数
subsample:サブサンプルの抽出割合
min_child_weight:子ノードの最小重みの制限
XGBoostにはハイパーパラメータが多数存在します。本来であればもっと細かい調整をしなくてはいけませんが、今回は特に重要な4つをグリッドサーチで交差検証します。
まずは検証するハイパーパラメータの値を作成しましょう。
1 2 3 4 5 6 7 8 9 |
# XGBoostのハイパーパラメータ の設定 param_cv_1 = { 'n_estimators': [10, 50, 100], 'max_depth': [5, 10], 'subsample': [0.5, 0.9], 'min_child_weight':[1, 5] } |
全部で9つのハイパーパラメータを検証します。Numpyのバージョンによっては警告が出ることがあります。動作は問題なく行いますので警告を非常にしちゃいましょう。
1 2 3 4 5 |
# 警告をオフにする import warnings warnings.filterwarnings('ignore') |
ではGridSearchCVで交差検証してみましょう。交差検証の回数は3回します。一般的なPCのスペックで1分くらい掛かりますので注意ください。
1 2 3 4 5 6 |
# グリッドサーチ clf = xgb.XGBClassifier(learning_rate=0.1, gamma=0.1, random_state=42) grid_1 = GridSearchCV(clf, param_grid = param_cv_1, cv=3, scoring='accuracy', verbose=1) grid_1.fit(X_train, y_train) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-- 出力 Fitting 3 folds for each of 24 candidates, totalling 72 fits [Parallel(n_jobs=1)]: Done 72 out of 72 | elapsed: 52.6s finished GridSearchCV(cv=3, error_score='raise', estimator=XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bytree=1, gamma=0.1, learning_rate=0.1, max_delta_step=0, max_depth=3, min_child_weight=1, missing=None, n_estimators=100, n_jobs=1, nthread=None, objective='binary:logistic', random_state=42, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, seed=None, silent=True, subsample=1), fit_params=None, iid=True, n_jobs=1, param_grid={'n_estimators': [10, 50, 100], 'max_depth': [5, 10], 'subsample': [0.5, 0.9], 'min_child_weight': [1, 5]}, pre_dispatch='2*n_jobs', refit=True, return_train_score='warn', scoring='accuracy', verbose=1) |
完了したので最も最適なハイパーパラメータの組み合わせを確認してみます。
1 2 3 4 |
# 最適なハイパーパラメータ を表示 grid_1.best_params_ |
1 2 3 4 |
--出力 {'max_depth': 5, 'min_child_weight': 1, 'n_estimators': 50, 'subsample': 0.9} |
グリッドサーチで交差検証を行なった結果、今回の候補の中で最もスコアが高いのは上記の組み合わせでした。では、テストデータを使って推測してみましょう。
1 2 3 4 5 6 7 |
# テストデータで推測 xgb_test_pred = grid_1.predict(X_test) # 混同行列を表示 confusion_matrix(y_test, xgb_test_pred, labels=[1, 0]) |
1 2 3 4 5 |
--出力 array([[ 341, 1298], [ 339, 1306]]) |
まぁこんなもんですよね(笑)。そもそも特徴量とか10分前の終値や安値と非常にシンプルなものですし。では正解率を確認してみます。
1 2 3 4 |
# 混同行列で確認 accuracy_score(y_test, xgb_test_pred) |
1 2 3 4 |
--出力 0.5015225334957369 |
ランダムに予測するのと大差ない感じです(笑)。より精度を上げるためには様々なハイパーパラメータの値を繰り返し交差検証を行なって、精度を改善していく必要があります。
では、いよいよ主役のoptunaで調整をしてみましょう!
STEP3 XGBoostをoptunaで調整
まずはoptunaのインストールからしましょう。pip経由で簡単にインストール可能です。ターミナルまたはコマンドプロンプトで下記を実行してください。
1 2 3 |
$ pip install optuna |
インストールが完了したらJupyterノートブックで下記を実行してoptunaをインポートしましょう。
1 2 3 4 |
# Optunaのインポート import optuna |
続いてXGBoostのインスタンスを生成します。
1 2 3 4 |
# XGBoostのインスタンス生成 xgboost_tuna = XGBClassifier(random_state=42) |
では、Optunaを使ってみましょう。詳しい操作方法は公式ドキュメンをご参照ください。Optunaですが目的関数を作って最適化を行うようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Objective Functionの作成 def opt(trial): n_estimators = trial.suggest_int('n_estimators', 0, 1000) max_depth = trial.suggest_int('max_depth', 1, 20) min_child_weight = trial.suggest_int('min_child_weight', 1, 20) subsample = trial.suggest_discrete_uniform('subsample', 0.5, 0.9, 0.1) colsample_bytree = trial.suggest_discrete_uniform('colsample_bytree', 0.5, 0.9, 0.1) xgboost_tuna = XGBClassifier( random_state=42, n_estimators = n_estimators, max_depth = max_depth, min_child_weight = min_child_weight, subsample = subsample, colsample_bytree = colsample_bytree, ) xgboost_tuna.fit(X_train,y_train) tuna_pred_test = xgboost_tuna.predict(X_test) return (1.0 - (accuracy_score(y_test, tuna_pred_test))) |
上の目的関数ですがXGBoostの調整したハイパーパラメータの候補値を設定して、訓練データで学習を行なった後にテストデータで推測して正解率を戻しています。
Optunaですが現在は最小化しか行えません。正解率を最適化する場合は本来は「最大化」ですが、あえて(1-正解率)とすることでOptunaの最小化に対応させています。(これが本当に正しい使い方なのかすこし不安が・・詳しい方がいればご指摘ください)
では、やってみましょう!上記の目的関数を使ってStudyオブジェクトで最適化を行います。 n_trials で検証回数を指定します。今回は100回の検証としました。一般的なPCで10分程度時間がかかるのでご注意ください。
1 2 3 4 5 |
# 最適化 study = optuna.create_study() study.optimize(opt, n_trials=100) |
1 2 3 4 5 6 7 8 9 10 11 |
--出力 [I 2018-12-21 22:31:01,628] Finished a trial resulted in value: 0.4923873325213155. Current best value is 0.4923873325213155 with parameters: {'n_estimators': 192, 'max_depth': 19, 'min_child_weight': 15, 'subsample': 0.8, 'colsample_bytree': 0.7000000000000001}. [I 2018-12-21 22:31:07,001] Finished a trial resulted in value: 0.49634591961023145. Current best value is 0.4923873325213155 with parameters: {'n_estimators': 192, 'max_depth': 19, 'min_child_weight': 15, 'subsample': 0.8, 'colsample_bytree': 0.7000000000000001}. [I 2018-12-21 22:31:07,370] Finished a trial resulted in value: 0.49573690621193667. Current best value is 0.4923873325213155 with parameters: {'n_estimators': 192, 'max_depth': 19, 'min_child_weight': 15, 'subsample': 0.8, 'colsample_bytree': 0.7000000000000001}. ・・・中略・・・ [I 2018-12-21 22:35:34,676] Finished a trial resulted in value: 0.4993909866017052. Current best value is 0.47868453105968334 with parameters: {'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9}. [I 2018-12-21 22:35:35,031] Finished a trial resulted in value: 0.49360535931790495. Current best value is 0.47868453105968334 with parameters: {'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9}. [I 2018-12-21 22:35:35,257] Finished a trial resulted in value: 0.49147381242387334. Current best value is 0.47868453105968334 with parameters: {'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9}. |
最適化が完了しました!結果を出力してみましょう。
1 2 3 4 5 6 |
# 結果を出力 print(study.best_params) print(study.best_value) print(study.best_trial) |
1 2 3 4 5 6 |
--出力 {'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9} 0.47868453105968334 FrozenTrial(trial_id=12, state=<TrialState.COMPLETE: 1>, value=0.47868453105968334, datetime_start=datetime.datetime(2018, 12, 21, 22, 31, 52, 170497), datetime_complete=datetime.datetime(2018, 12, 21, 22, 31, 52, 746600), params={'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9}, user_attrs={}, system_attrs={}, intermediate_values={}, params_in_internal_repr={'n_estimators': 24, 'max_depth': 20, 'min_child_weight': 18, 'subsample': 0.5, 'colsample_bytree': 0.9}) |
Optunaで最適化を行なった結果、ベストスコアは0.4786と出ています。これは前述した通り(1-正解率)のスコアですので、正解率に直すと0.5214です。
では、Optunaが導き出した最適なハイパーパラメータを使ってテストデータで推測してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 最適なハイパーパラメータを設定 fin_xgboost = XGBClassifier( random_state=42, n_estimators= 30, max_depth=13, min_child_weight=11, subsample=0.9, colsample_bytree=0.5, ) # モデル訓練 fin_xgboost.fit(X_train, y_train) # テストデータで推測値を算出 fin_test_pred = fin_xgboost.predict(X_test) # 混同行列で確認 confusion_matrix(y_test, fin_test_pred, labels=[1, 0]) |
1 2 3 4 5 |
--出力 array([[ 471, 1168], [ 413, 1232]]) |
明らかにグリッドサーチの結果よりも改善しているのが確認できますね。では正解率を算出してみましょう。
1 2 3 4 |
# 正解率を算出 accuracy_score(y_test, fin_test_pred) |
1 2 3 4 |
--出力 0.5185749086479903 |
グリッドサーチでは0.5015に対して、Optunaは0.5185と約1.7ポイント近く改善しているのが確認できます!
まとめ
今回は日本のAIシーンを牽引しているPreferred Networksさんが発表したハイパーパラメータ最適化フレームワーク「Optuna」を使ってXGBoostの調整を行なってみました。
実際に使ってみた感想としては直感的に動かせるので大ファンになりました!今までもベイズ最適化を使ったフレームワークはHyperOptなどいくつか出ていましたが、これからはOptunaを間違いなく使うと思います!
ブログ読んでいただきありがとうございます!Twitterでも色々と発信しているので、是非フォローお願いします!
ディスカッション
コメント一覧
まだ、コメントがありません