XGBoostをOptunaでチューニング（コード解説付き）

##はじめに##
この記事では[前回の記事（TFLearn使ってみた。Tensorflowと比較。）](https://qiita.com/Hatomugi/items/bd5a8cadf0c19e3ec06a)で使ったデータを今回はXGBoost + Optunaで使って結果を比較してみます。ついでにkeras + Optunaでもやります。

目次
1. Optunaとは
2. XGBoost + Optunaの実装
3. 局所のコード解説
4. 結果 (XGBoost)
5. keras + Optunaの実装
6. 結果 (Keras)
7. 参考文献

##Optunaとは##
一言でいえば<b>パラメーターチューニングを効率良く自動化したフレームワーク</b>。ハイパーパラメータの値を試行錯誤しながら、そのモデルに最適な値を探すことを可能とします。

（ハイパーパラメータとは機械学習アルゴリズムの挙動を制御するためにプログラマが付けるパラメータです。例としてバッチ数や学習率が挙げられます。単なるパラメータとの違いを挙げるとすれば、パラメータがfunction(parameters)など既に決まっている値に対し、ハイパーパラメータはプログラマ自身が色々と試行錯誤しながら決めていく値という感覚で大丈夫です）

##Optunaの実装##

```py
import pandas as pd
import numpy as np
from sklearn.model_selection import KFold
from sklearn.metrics import roc_auc_score, accuracy_score
from sklearn.model_selection import train_test_split
import xgboost as xgb
import optuna

#Objective関数の設定
def objective(trial):
	data = pd.read_csv('a.csv')
	x, y = pre_processing(data)

	x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=0)

	params = {
    	'objective': 'binary:logistic',
    	'max_depth': trial.suggest_int('max_depth', 1, 9),
    	'n_estimators': trial.suggest_int('n_estimators', 10, 1000),
    	'learning_rate': trial.suggest_loguniform('learning_rate', 1e-8, 1.0)
    }

	model = xgb.XGBClassifier(**params)
	model.fit(x_train, y_train)

	pred = model.predict(x_test)

	accuracy = accuracy_score(y_test, pred)
	return (1-accuracy)

if __name__ == '__main__':

	study = optuna.create_study()
	study.optimize(objective, n_trials=300)

	print(study.best_params)
	print(study.best_value)
	print(study.best_trial)
```
（簡略化するためにpre_processing()の実装は省いています。詳しくは[前記事](https://qiita.com/Hatomugi/items/bd5a8cadf0c19e3ec06a)を参照してください）

それでは公式読まずにさっさとコードを理解したいという方に抽象から具体の流れで説明していきます。

直観的に理解する全体の流れ

「<b>XGBoostを一定回数訓練する。その一回一回の訓練中に違うパラメーターを試してみて一番性能を発揮したパラメータを最後にユーザーへ出力する。</b>」


具体的に説明

「<b>study.optimize()の引数のn_trials回だけXGBoostを訓練する。そして一回一回の訓練中にparams{}の中のtrial.suggest()等と設定されているパラメーターを試す。その後（今まで試したパラメータの履歴も参照しながら）次に試すパラメータを、有望な領域から定める。これをn_trials回分繰り返し、完了したあとに最も最適だったパラメータをユーザーに出力する。</b>」

この有望な領域というのは、Tree-structured Parzen Estimator というベイズ最適化アルゴリズムの一種を用いて出しています。
（参考：https://research.preferred.jp/2018/12/optuna-release/
）

（注：一回一回でパラメータを変えるとありますが、変えないときもあります。説明の簡略化のために省きました）

全体の流れをステップで表すとこんな感じ。
1. studyオブジェクト生成。
2. optimize()実行
3. trial.suggest_〇〇からパラメータを決める。
4. 訓練開始
5. returnで1-accurayを返す（できるだけ1-accuray=0に近づけたい）
6. returnの結果も見つつ次のパラメーター候補を定める。（詳しくはベイズ最適化参照）
6. 1~4をn_trials回繰り返す。

それでは局所のコード解説に入ります。

まずobjective()の引数にtrialというのがありますが、

```py
def objective(trial):
```
これはobjective関数が呼ばれるたびに内部で自動的に生成される[Trialというクラス](https://optuna.readthedocs.io/en/latest/reference/trial.html#optuna.trial.Trial)のインスタンスです。このインスタンスはtrial.suggest_〇〇〇()等からわかるようにハイパーパラメータの値をサンプリングする役割を果たします。


```py
study = optuna.create_study()
study.optimize(objective, n_trials=300)
```
ここではcreate_studyという関数が使われています。これは文字通りstudyオブジェクトを生成しています。studyオブジェクトとは最適化を担う（扱う）オブジェクトです。

```
params = {
    'objective': 'binary:logistic',
    'max_depth': trial.suggest_int('max_depth', 1, 9),
    'n_estimators': trial.suggest_int('n_estimators', 10, 1000),
    'learning_rate': trial.suggest_loguniform('learning_rate', 1e-8, 1.0)
    }
```
実際のハイパーパラメータの設定です。例えばこのコード内のmax_depthは「1から9までの整数内でパラメータを決めてくれ」と言っています。

ご存知かと思いますがsuggestは「提案する」という意味なので覚えやすいですね。

```py
return (1-accuracy)
```
最後に(1-accuracy)を返します。study.optimize(objective, n_trials) は objective関数を最適化するための関数ですが、これはobjective関数内のaccuracyをできる限り1.0に近づけて、(1-accuracy) = 0にしたいという意味です。

##結果 (XGBoost)##

```
 'objective': 'binary:logistic',
 'max_depth': 5,
 'n_estimators' : 242,
 'learning_rate': 0.099326350132149599
```

実際にkfold=5のXGBoostで試してみるとAccuracyは0.869ぐらい。パラメーターチューニングの回もあってかTensorflowより若干良かった。

##Kerasでの実装##

```py
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras import optimizers
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
import optuna

def objective(trial):

	data = pd.read_csv('a.csv')

	x_train, y_train = pre_processing(data)

	x_train = scaling(x_train)

	units  = trial.suggest_int("units", 3, 65)
	activation = trial.suggest_categorical("activation", ["relu", "tanh", "sigmoid"])
	optimizer = trial.suggest_categorical("optimizer", ["sgd", "adam", "rmsprop"])

	model = Sequential()

	model.add(Dense(units=units, input_shape=(11,), activation=activation))
	model.add(Dense(units=units, activation=activation))
	model.add(Dense(units=1, activation='sigmoid'))

	model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])

	history = model.fit(x_train, y_train, batch_size=10, verbose=0, validation_split=0.2, epochs=5)

	return (1 - history.history["val_acc"][-1])

if __name__ == '__main__':
	study = optuna.create_study()
	study.optimize(objective, n_trials=100)

	print(study.best_params)	
	print(study.best_value)
	print(study.best_trial)
```
（簡略化するためにpre_processing()とscaling()の実装は省いています。詳しくは[前記事](https://qiita.com/Hatomugi/items/bd5a8cadf0c19e3ec06a)を参照してください）

肝心の結果ですが手違いで紛失しました（笑）。ただ[前の記事](https://qiita.com/Hatomugi/items/bd5a8cadf0c19e3ec06a)のハイパーパラメータとほぼ同じ結果になりました。学習用のデータセットを使ったからだと思われます。

注意点としてepoch数を増やすとそれだけ時間がかかるためGCPで実行することをお勧めします。また筆者のようにepoch数を減らすと過学習を起こすのでお勧めはしません。例えば10エポックと指定した場合、10エポック時点での最適なパラメータを探してしまうので10エポックでの結果がベストでも100エポックしたときにベストとは限りません。またエポック自体をOptunaに自動で探索してもらうのもありでしたが、大分重くなりそうなので止めました。


##参考文献：##
https://optuna.readthedocs.io/en/latest/index.html
https://optuna.readthedocs.io/en/latest/reference/trial.html#optuna.trial.Trial
https://optuna.readthedocs.io/en/latest/reference/study.html#optuna.study.Study
https://www.hellocybernetics.tech/entry/2018/12/22/160349
https://research.preferred.jp/2018/12/optuna-release/