XGBoostをoptunaでチューニングしてtitanicを解く

# はじめに
titanic号の乗客のうち、生き残るのは誰かをXGBoostを使って予測した。
その際、ハイパーパラメータはoptunaで自動チューニングした。
titanic号の乗客のデータセットはKaggleからダウンロードしました。
# 実行環境
* python 3.6.6
* XGBoost 0.81
* optuna 0.6.0

# 実装
## 方針
1. xgboostだけ。自分で決める必要があるパラメータは適当に決める
2. optunaで(1)でやったパラメータをチューニングする
3. パラメータを増やす
データは891人分あるので、(学習データ:テストデータ)=(791:100)でやる。

## ライブラリ
```python:import
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
```  

## データ前処理
```python:preprocess
def preprocess(df):
    df['Fare'] = df ['Fare'].fillna(df['Fare'].mean())
    df['Age'] = df['Age'].fillna(df['Age'].mean())
    df['Embarked'] = df['Embarked'].fillna('Unknown')
    df['Sex'] = df['Sex'].apply(lambda x: 1 if x == 'male' else 0)
    df['Embarked'] = df['Embarked'].map( {'S':0,'C':1,'Q':2,'Unknown':3} ).astype(int)
    df = df.drop(['Cabin','Name','PassengerId','Ticket'],axis=1)
    return df
```
引数dfのデータセットを正規化して返す関数です。
学習に利用したデータは

* Survived (生存したかどうか)
* Pclass (乗客の階級)
* sex
* Age
* SibSp (兄弟、配偶者の数)
* Parch (両親、子供の数)
* Fare (乗船料金)
* Embarked (乗船した港)
です。
使わないデータはdropしています。
必要なデータの欠損データは平均で埋めて、型はINT型に直してます。

## 方針1(xgboostのみ)
### 学習（xgboost）
まずはxgboostだけのもの

```python:train
def train(df,max_depth,learning_rate,num_round):
    train_x = df.drop('Survived',axis=1)
    train_y = df.Survived
    dtrain = xgb.DMatrix(train_x,label=train_y)
    param = { 'max_depth':max_depth,'learning_rate':learning_rate,'objective':'reg:logistic' }
    bst = xgb.train(param,dtrain,num_round)
    return bst
```

学習モデルbstを作成して返す関数です。
引数の説明

* df : 学習用データセット
* max_depth : ツリーの最大の深さ
* learning_rate : 学習率
* num_round : ブースティングを行う回数

Paramのobjectiveで最小化させる損失関数を指定します。今回は、reg:logistic(logistic回帰)を指定しています。

### 学習モデルの予測値を返す関数
```python:predict
def predict(bst,df):
    return bst.predict(xgb.DMatrix(df))
```
名前の通りです。

### main関数
```python:main.py
def main():
    df_original = pd.read_csv("/Users/pc1013/Desktop/first/XGBoost_optuna/df/train.csv")
    df_test = preprocess(df_original.tail(100))
    df_train = preprocess(df_original.head(791))
    y = df_test['Survived']
    df_test = df_test.drop('Survived',axis=1)
        
    max_depth = 3
    learning_rate = 0.6
    round_num = 5
    
    bst = train(df_train,max_depth,learning_rate,round_num)
    answer = predict(bst,df_train.drop('Survived',axis=1)).round().astype(int)
    print('train score :',accuracy_score(answer.round(),df_train['Survived']))
    answer = predict(bst,df_test).round().astype(int)
    xgb.plot_importance(bst)
    print('test score :',accuracy_score(answer.round(),y))
```
main関数です。
とりあえず、ハイパーパラメータは

* max_depth = 3 
* learning_rate = 0.6
* num_round = 5

としてみます。

## 方針2(xgboost+optuna)
optunaで自動チューニングする時に必要なのが、

* 目的関数を最適化するobjective()関数
* optunaの準備とobjective()の実行(main関数の変更)
### objective()について
```python:objective
def objective(df, df_test, y,trial):
    #目的関数
    max_depth = trial.suggest_int('max_depth',1,30)
    learning_rate = trial.suggest_uniform('learning_rate',0.0,1)
    round_num = trial.suggest_int('round_num',1,30)

    bst = train(df,max_depth,learning_rate,round_num,gamma,min_childe_weigh,subsample,colsample_bytree,alpha,lamb)
    answer = predict(bst,df_test).round().astype(int)
    score = accuracy_score(answer.round(),y)
    return 1.0 - score
```
max_depthとround_numは[1,30]の整数値
learning_rateは[0.0,1.0]の少数値
と言う範囲でオートチューニングします。
評価は毎回テストデータで行うので、7行目`predict(bst,df_test)`は引数にdf_testを入れてます。

### main()  
```python:main.py
def main():
    df_original = pd.read_csv("/Users/pc1013/Desktop/first/XGBoost_optuna/df/train.csv")
    df_test = preprocess(df_original.tail(100))
    df_train = preprocess(df_original.head(791))
    y = df_test['Survived']
    df_test = df_test.drop('Survived',axis=1)
    
    #optunaの前処理
    obj_f = partial(objective, df_train, df_test, y)
    #セッション作成
    study = optuna.create_study()
    #回数
    study.optimize(obj_f, n_trials=100)
    
    max_depth = study.best_params['max_depth']
    learning_rate = study.best_params['learning_rate']
    round_num = study.best_params['round_num']
    
    bst = train(df_train,max_depth,learning_rate,round_num)
    print('\nparams :',study.best_params)
    answer = predict(bst,df_train.drop('Survived',axis=1)).round().astype(int)
    print('train score :',accuracy_score(answer.round(),df_train['Survived']))
    answer = predict(bst,df_test).round().astype(int)
    xgb.plot_importance(bst)
    print('test score :',accuracy_score(answer.round(),y))
```
変更は8行目から。
obj_fではobjective()の引数を与えています。
n_trialはチューニングの試行回数で、
10から増やしいき50くらいから、増やしても変化があまりみられなかったので、今回は100回としてみました。

`study.best_params[element]`でハイパーパラメータ(element)のチューニング結果を得られます。  
これを使って、モデルを作成し、評価します。

## 方針3(optunaで沢山パラメータチューニング)
新たに、今までデフォルトだったパラメータたち

* alpha
* gamma
* min_childe_weigh
* colsample_bytree

をチューニングします。
それぞれのパラメータについては、調べてもなんとなくしかわかりませんでした。
subsampleとlambはか学習を防ぐためのパラメータですが、モデルが過学習しそうになかったため除外して
subsample = 1
lanb = 1

objectice()とmain()については、パラメータ増やすだけなので、コードは省略。
パラメータの範囲だけ⬇︎

* gamma - [0.0,1.0]
* subsample - [0.0,1.0]
* colsample_bytree - [0.0,1.0]
* min_childe_weigh - [0.0,1.0]
* alpha - [0.0,1.0]

# 実行結果

## 結果1(xgboostのみ)
精度は、85.0%
学習データ精度85.5%→過学習なし。
寄与率
<img width="432" alt="titanic_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/293940/097bec3b-f7ed-2636-91c4-f39da06fed82.png">

## 結果2(xgboost+optuna)
精度は、89.0%
学習データ精度94.5%→過学習なし?
寄与率
<img width="430" alt="titanic_2.png" src="https://qiita-image-store.s3.amazonaws.com/0/293940/58512752-19af-df4f-0773-302991470e18.png">

## 結果3(xgboost+optuna+沢山パラメータ)
精度は、89.0%
学習データ精度88.2%→過学習なし。
寄与率
<img width="435" alt="titanic_3-0.png" src="https://qiita-image-store.s3.amazonaws.com/0/293940/6c2cded5-bf44-7ace-2a69-da7eb50a36cb.png">

trial回数100
xgboostのアルゴリズムをbinary:logistic(ロジスティック2項分類)にして実行したところ、
精度97.5%が出ました。
9割超えた！
寄与率
<img width="440" alt="titanic_3-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/293940/59d773ff-79be-d4d5-73f1-513466e44521.png">

# まとめ
XGBoostが既にすごい。
optuna使うとちょっと精度上がる。
パラメータ増やすと安定する。

# 追記
過ちに気づいたので、追記.
本記事では、データを
「 データ → 学習データ + テストデータ 」
と分割し、テストデータを使った損失関数の出力から、パラメータをチューニングしていた.
しかし、これをテストデータでテストすると、本来より精度が高く出る可能性がある．ハイパーパラメータがオーバーフィットする可能性があるからだ.

よって、正しく精度を測るには
「 データ → 学習データ + **検証データ** + テストデータ 」
というように分割し、パラメータチューニングは検証データで行い、チューニング後にテストデータで精度を測る必要がある.