KaggleでOptunaを用いてXGBoostのハイパーパラメータをチューニングする

#　概要
東大GCIのデータサイエンティスト育成講座内で行われたコンペティションの上位解法の中に、Optunaを用いてXGBoostのハイパーパラメータを決定したというのを見かけました。

その時は、機械学習初心者(今も)だったので、「ふーん、そんなのがあるんだ」程度に見ていたんですが、いざKaggleのNCAAコンペでモデリングに取り組んでみると、ハイパーパラメータをどうやって決定するかで、悩みに悩んでいたところ、「Optunaというものがあったなぁ」と思い出したので、公式チュートリアルを見ながら自分流に調べて実装してみたものをまとめておきます。ちなみにQiita初投稿です。
#　Optuna
Optunaは自動ハイパーパラメータ最適化ソフトウェアフレームワークであり、特に機械学習のために設計されたものであると書かれています。
先に、自分流のOptunaの使い方の流れを説明すると、
　1.スコア(値が小さいほど良いスコア)を返す関数を作る
　2.optuna.create_studyクラスのインスタンスにその関数を渡す
という風になります。
実際に、
$$(x-2)^{2}$$
というスコアが最小化されるようなパラメータxを求めてみます。
(以下公式チュートリアルに記載されているものを少し改変)

```python3:seek_for_x.py
import optuna

def objective(trial): #上記の1で述べた関数を作成
  x = trial.suggest_uniform('x', -10, 10) #-10以上10未満の連続値(suggest_uniform)
  return (x - 2) ** 2

study = optuna.create_study() #上記の2で述べたインスタンスを作成
study.optimize(objective, n_trials=100) #１００回ベストパラメータの探索が行われる。

print("一番スコアがよかったパラメータ: {}".format(study.best_params))
print("一番良かったスコア: {}".format(study.best_value))
print("一番良かった試行: {}".format(study.best_trial))
print("全ての試行: {}".format(study._trials))

```

```python3:実行結果
一番スコアがよかったパラメータ:　{'x': 1.9487825780924659}
一番良かったスコア:　5.390694980884334e-05
一番良かった試行:　FrozenTrial(number=26, state=<TrialS...
全ての試行: [FrozenTrial(number=0, state=<TrialS ...
```

![aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435614/a27b68c6-0942-a2b8-32bb-30e29de44b28.png)

　　　　**(x軸がxで、y軸はobjective関数の返り値)**
optunaで求めたパラメータxは実際の目的関数のグラフを見てみても妥当な結果になっています。
Optunaは、objective関数を最小にしてくれるようなハイパーパラメータを求めてくれるということです。

(*これはパラメータが一個だったため可視化できたが、パラメータが複数になると可視化もできなくなり、最小値を求めるのも難しくなる。*)

#　Optunaを用いたXGBoostのチューニング
その前に、今回扱うXGBoostのハイパーパラメータについてまとめておきます。
(XGBoostのハイパーパラメータは、今回説明するもの以外にもある。)


|　パラメータ         | 説明                                   | 探索範囲          |
|:-----------------|:--------------------------------------|:------------------|
| eta              |学習率                                  | 0.05 ~ 0.1       |
| max_depth        |決定木の深さ                             | 3 ~ 9             |
| min_child_weights|葉を分岐するために必要最小なデータ数          |1e-1 ~ 1e+1        |
| gamma            |決定木を分岐させるために必要な目的変数減少度合い|1e-8 ~ 1.0         |

実際にコンペで試しに書いてみたモデリング部分のコードです。

```python3:train.py
import xgboost as xgb
import optuna
from sklearn.metrics import log_loss

dtrain = xgb.DMatrix(X_train, label=y_train)
dvalid = xgb.DMatrix(X_valid, label=y_valid)
dtest = xgb.DMatrix(X_test, label=y_test)

def objective(trial): #上記の1で述べた関数を作成
    eta = trial.suggest_uniform('eta', 0.05, 0.1) #0.05以上0.1未満の連続値（suggest_uniform)
    max_depth = trial.suggest_int('max_depth', 3, 9) #3以上9以下の整数(suggest_int)
    min_child_weights = trial.suggest_loguniform('min_child_weights', 1e-1, 1e+1)#1e-1以上1e+1未満の連続値(suggest_loguniform)
    gamma = trial.suggest_loguniform('gamma', 1e-8, 1.0) #1e-8以上1.0未満の連続値(suggest_loguniform)
    evals = [(dtrain, 'train'), (dvalid, 'valid')]
    params = {'objective': 'binary:logistic', 'eta': eta, 'max_depth': max_depth, 'min_child_weights': min_child_weights, 'gamma': gamma}

    bst = xgb.train(params, dtrain, evals=evals, num_boost_round=1000, early_stopping_rounds=10)
    bst = xgb.train(params, dtrain, evals=evals, num_boost_round=bst.best_iteration)
    score = log_loss(y_valid, bst.predict(dvalid)) #評価指標はlogloss
    return score

study = optuna.create_study() #上記2で述べたインスタンスを作成
study.optimize(objective, n_trials=10)

print("best params\n{}".format(study.best_params)) #ベストパラメータを表示
print("best score {}".format(study.best_value)) #ベストスコアを表示
print('best trial {}'.format(study.best_trial)) #ベストスコアを叩き出した試行を表示
```

```python3:実行結果
best params
{'eta': 0.0642554427123927, 'max_depth': 3, 'min_child_weights': 2.8140752958557407, 'gamma': 0.0004531127090932686}

best score 0.5879874329136726

best trial FrozenTrial(number=5, value=0.5879...)
```

#　まとめ
GridSearchでパラメータの範囲を全て探索するときと比べると、計算時間もかなり少ないので良いなと思います。まだまだ機械学習初心者なのでこれからも地道に頑張ります。