ハイパーパラメータの調整

**パラメータ**はモデルの学習実行後に得られる値で、**重み**とも呼ばれます。
対して**ハイパーパラメータ**は各アルゴリズムに付随して、**アルゴリズムの挙動を制御するための値**です。
ハイパーパラメータを調整することで、っモデルの性能向上や過学習の抑制、効率の良い学習などが期待できます。

# 1. K-分割交差検証 (K-fold cross-validation)
与えられたデータセットを「学習用データセット」「テスト用データセット」に2分割することをホールドアウト法と呼びます。
ホールドアウト法では、ハイパーパラメータの調整に使うデータがありませんので、ハイパーパラメータを調整するためには「検証用データセット」を用意する必要があります。

しかし、3分割するほどデータ量がないこともあります。
その時に使うのが、**K-分割交差検証 (K-fold cross-validation)**です。

【手順】
1.データセットをK個に分割
2.分割したデータの1個を検証用データセットとし、残りK-1個を学習用データセットとして学習を実行
→ 1 回で学習を終わらせず、計K回の学習を行います。その際、既に検証用データセットに使ったデータを次は学習用データセットとして使用し、新たに検証用データセットを選択します。
3.各検証の結果を平均して最終的な検証結果とする

# 2. ハイパーパラメータの調整方法
ハイパーパラメータの調整するには、以下のような方法があります。
1.手動での調整
2.グリッドサーチ (Grid Search)
3.ランダムサーチ (Random Search)
4.ベイズ最適化 (Bayesian Optimization)

## 2-1. 手動での調整
`DecisionTreeClassifier`で決定木の実装を行う場合を考えます。

```python
from sklearn.tree import DecisionTreeClassifier
# ハイパーパラメータ調整なし
dtree = DecisionTreeClassifier(random_state=0)
# ハイパーパラメータ調整あり
dtree2 = DecisionTreeClassifier(max_depth=10, min_samples_split=30, random_state=0)
```
このように、`max_depth`や`min_samples_split`について、自分で適当に値を入れて、数値を変えるたびに予測精度を確認するという方法が、手動での調整です。

## 2-2. グリッドサーチ (Grid Search)
グリッドサーチはまず、ハイパーパラメータを探索する範囲を決めます。
例えば上記のように決定木の`max_depth`と`min_samples_split`の値を調整したい場合、5、10、15、20、25 のように範囲をそれぞれ決めます（範囲の指定に特に決まりはありません）。
この場合のハイパーパラメータの組み合わせは 5x5=25 個になります。
この25個のハイパーパラメータの組み合わせ全てを使用して、学習・検証を行います。
そして、その結果から予測精度が最も高いハイパーパラメータを採用します。

グリッドサーチには以下の3つを準備する必要があります。

- estimator ： 学習に使用するモデル 
- param_grid ： ハイパーパラメータを探索する範囲 
- cv ： K-分割交差検証のKの値

```python
# GridSearchCV クラスのインポート
from sklearn.model_selection import GridSearchCV

# 学習に使用するアルゴリズムの定義
estimator = DecisionTreeClassifier(random_state=0)

# 探索するハイパーパラメータと範囲の定義
param_grid = [{
    'max_depth': [3, 20, 50],
    'min_samples_split': [3, 20, 30]
}]

# データセット分割数を定義
cv = 5

# GridSearchCV クラスを用いたモデルの定義
tuned_model = GridSearchCV(estimator=estimator, 
                           param_grid=param_grid, 
                           cv=cv, return_train_score=False)

# モデルの学習＆検証
tuned_model.fit(x_train_val, t_train_val)

# 検証結果の確認
pd.DataFrame(tuned_model.cv_results_).T
```
ここまで入力すると、それぞれのハイパーパラメータの値に対する予測精度などが表になって出力されます。
`mean_test_score`の値を見ると、モデルの予測精度がわかります。
その後、結果を参照して先ほどより狭い範囲でハイパーパラメータを調整します。
これを何度か繰り返すことで徐々に予測精度が高くなるハイパーパラメータへと近づけて行きます。

```python
# 最も予測精度の高かったハイパーパラメータの確認
tuned_model.best_params_

# 最も予測精度の高かったモデルの引き継ぎ
best_model = tuned_model.best_estimator_

# モデルの検証
print(best_model.score(x_train_val, t_train_val))
print(best_model.score(x_test, t_test))
```
これで、最も予測精度が高かったハイパーパラメータのモデルで検証することができます。

## 2-3. ランダムサーチ (Random Search)
ランダムサーチは指定した範囲のハイパーパラメータをランダムに抽出し、学習・検証を行います。
グリッドサーチに比べて、より広い範囲を探索することができるので、効率的と言われています。
しかし、全てのハイパーパラメータを探索するわけではないので、本当に最適か判断が難しいと言えます。
ランダムサーチで大体のあたりをつけてから、グリッドサーチで細かく見ていくやり方だと双方のメリットが活かせます。

```python
# RandomizedSearchCV クラスのインポート
from sklearn.model_selection import RandomizedSearchCV
# 学習に使用するアルゴリズム
estimator = DecisionTreeClassifier(random_state=0)

# ハイパーパラメータを探索する範囲の指定 range(開始値, 終了値, ステップ)
# 例えば range(1, 10, 2) の場合、1から10までの値を2刻みで獲得できる。その値をlist()でリスト化。
param_distributions = {
    'max_depth': list(range(5, 100, 2)),
    'min_samples_split': list(range(2, 50, 1))
}

# 試行回数の指定
n_iter = 100

# Kの値を指定
cv = 5

# モデルの定義
tuned_model = RandomizedSearchCV(
    estimator=estimator, 
    param_distributions=param_distributions, 
    n_iter=n_iter, cv=cv, 
    random_state=0, return_train_score=False
)

# モデルの学習＆検証
tuned_model.fit(x_train_val, t_train_val)

# 学習結果の確認（スコアの高い順に表示）
pd.DataFrame(tuned_model.cv_results_).sort_values('rank_test_score').T
```
ここまで入力すると、それぞれのハイパーパラメータの値に対する予測精度などが表になって出力されます。

```python
# 最も予測精度の高かったハイパーパラメータの確認
tuned_model.best_params_

# 最も予測精度の高かったモデルの引き継ぎ
best_model = tuned_model.best_estimator_

# モデルの検証
print(best_model.score(x_train_val, t_train_val))
print(best_model.score(x_test, t_test))
```

## 2-4. ベイズ最適化 (Bayesian Optimization)
数学的背景の理解が難しいのですが、ベイズ最適化では探索（この辺かな〜？と適当に入力してみる）や活用（結果をもとに、さらにこの辺かな〜？と値を更新する）で得られた情報を元にハイパーパラメータを調整していくため、予測精度が高くなるハイパーパラメータを見つけやすいそうです。
日本の Prefferd Networks 社が開発している Optuna というフレームワークを使用するときの方法を記述します。

```python
# optuna のインストール
!pip install optuna
import optuna
```
Optunaでは最初に関数`objective`を定義して内部に以下の要素を関数として順に定義します。

- 1.ハイパーパラメータごとに探索範囲を指定
- 2.学習に使用するアルゴリズムを指定
- 3.学習の実行、検証結果の表示

探索範囲の指定にはデフォルトで準備されている`trial`クラスを使用します。
3では学習・検証を繰り返してハイパーパラメータの調整を行うのですが、その際に`return`で取得した検証結果を最小化（最大化）するように調整が進みます。
また、3でK-分割交差検証を使用するには`cross_val_score`が必要です。

```python
from sklearn.model_selection import cross_val_score
def objective(trial, x, t, cv):
    # 1. ハイパーパラメータごとに探索範囲を指定
    max_depth = trial.suggest_int('max_depth', 2, 100)
    min_samples_split = trial.suggest_int('min_samples_split', 2, 100)

    # 2. 学習に使用するアルゴリズムを指定
    estimator = DecisionTreeClassifier(
      max_depth = max_depth,
      min_samples_split = min_samples_split
    )

    # 3. 学習の実行、検証結果の表示
    print('Current_params : ', trial.params)
    accuracy = cross_val_score(estimator, x, t, cv=cv).mean()
    return accuracy
```
正解率の最大化を目的とする場合には、direction='maximize' を指定します。

```python
#　 studyオブジェクトの作成（最大化）
study = optuna.create_study(direction='maximize')

# K分割交差検証の K
cv = 5

# 目的関数の最適化
study.optimize(lambda trial: objective(trial, x_train_val, t_train_val, cv), n_trials=10)

print(study.best_trial)
```
学習が終了したので、最も予測精度の高かったハイパーパラメータを確認するために`study.best_params`を実行します。

```python
# 最も予測精度の高かったハイパーパラメータの確認
study.best_params

# 最適なハイパーパラメータを設定したモデルの定義
best_model = DecisionTreeClassifier(**study.best_params)

# モデルの学習
best_model.fit(x_train_val, t_train_val)

# モデルの検証
print(best_model.score(x_train_val, t_train_val))
print(best_model.score(x_test, t_test))
```