scikit-learn を用いた交差検証（Cross-validation）とハイパーパラメータのチューニング（grid search）

#はじめに
本記事は [pythonではじめる機械学習](https://www.oreilly.co.jp/books/9784873117980/) の 5 章（モデルの評価と改良）に記載されている内容を簡単にまとめたものになっています．

具体的には，python3 の scikit-learn を用いて

- 交差検証（Cross-validation）による汎化性能の評価
- グリッドサーチ（grid search）と呼ばれる方法でハイパーパラメータの調整

を行う方法についてのまとめです．

記事内で用いられる学習モデル（サポートベクターマシン等）の詳細や python の導入方法や文法などは省略しています．

#交差検証（Cross-validation）
iris のクラス分類を行う場合を例に考えてみます．

教師あり学習なので，まずはデータセットを training set と test set に分割し，training set によるモデルの学習，test set によるモデルの評価を行います．

```python
# 必要なライブラリの import
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

# データのロード
iris = load_iris()

# データの分割
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)

# training set を用いて学習
logreg = LogisticRegression().fit(X_train, y_train)

# test set を用いて評価
score = logreg.score(X_test, y_test)
print('Test set score: {}'.format(score))
```

出力結果

```
Test set score: 0.868421052631579
```

ここで得られたスコアははじめに実行された train_test_split() により得られた training set に依存しています．
しかし，本来は未知のデータに対する予測制度，つまり，汎化精度を高めることが目的なので，より頑健な汎化精度の評価方法が必要となります．

そこで用いられるのが交差検証（Cross-validation）です．

交差検証では，データセットを $k$ 個に分割し，モデルの訓練と評価を $k$ 回行います．
得られた $k$ 個の評価値の平均をとった値を最終的なモデルのスコアとして扱います．

具体的な流れは次の通りです．

1. データを $k$ 個のブロックに分ける．これを分割（fold）という．
2. 最初の分割1 を test set，残りの分割2~5 を training set とし，モデルの学習と評価を行う．
3. 分割2 を test set，残りの分割1, 3~5 を training set として，モデルの学習と評価を行う．
4. この過程を，分割3, 4, 5 を test set として繰り返す．
5. 得られた $k$ 個の精度の平均値をモデルの評価値とする．

Scikit-learn では，上記の流れを model_selection の cross_val_score() 関数を用いることで簡単に実行できます．

```python

from sklearn.model_selection import cross_val_score
logreg = LogisticRegression()
# 交差検証
scores = cross_val_score(logreg, iris.data, iris.target)
# 各分割におけるスコア
print('Cross-Validation scores: {}'.format(scores))
# スコアの平均値
import numpy as np
print('Average score: {}'.format(np.mean(scores)))
```

出力結果

```
Cross-Validation scores: [ 0.96078431  0.92156863  0.95833333]
Average score: 0.9468954248366014
```

cross_val_score() の引数に機械学習モデルとデータセットを渡すことで，各分割における評価値のリストが得られます．
分割数 $k$ はパラメータ cv で指定することができ，デフォルトでは $k=3$ となっています．
評価値の平均値は numpy の mean() 関数で簡単に計算することができます．

交差検証により，できるだけデータの分割によらない頑健な汎化精度の評価が可能となります．

## データの様々な分割方法
データを分割する方法にも様々な種類が存在します．

単純な方法では，データの最初から $1/k$ ずつ分割します．

層化 $k$ 分割交差検証（Stratified $k$-fold cross-validation）と呼ばれる方法では，各分割内でのクラスの比率が全体の比率と同じになるように分割します．

cross_val_score() はパラメータ cv を用いることで分割方法を指定することができます．

```python
from sklearn.model_selection import KFold
from sklearn.model_selection import StratifiedKFold
# 単純な方法
kfold = KFold(n_splits=3)
print('Cross-validation scores: \n{}'.format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
# 層化 k 分割交差検証
stratifiedkfold = StratifiedKFold(n_splits=3)
print('Cross-validation scores: \n{}'.format(cross_val_score(logreg, iris.data, iris.target, cv=stratifiedkfold)))
```

出力結果

```
# 単純な方法
Cross-validation scores:
[ 0.  0.  0.]
# 層化 k 分割交差検証
Cross-validation scores:
[ 0.96078431  0.92156863  0.95833333]
```

iris のデータセットは 3 つのクラスが 50 個ずつ，計 150 個存在し，以下のように各クラスのデータが順番に並んでいます．

```
print(iris.target)
# 出力結果
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2]
```

ゆえに，単純な分割方法では training set と test set に含まれるデータのクラスに大きな偏りが発生してしまい，精度をうまく測定することができません．

分割方法をうまく選択することも汎化精度を測定する上で大事なポイントとなります．

一般的には，回帰には単純な $k$ 分割交差検証，クラス分類には層化 $k$ 分割交差検証が用いられます．
cross_val_score() のパラメータ cv に何も指定しない場合はこの選択基準で分割方法が選択されます．

他にも大規模データセットに対して有効な 1 つ抜き交差検証（leave-one-out）などがありますが，詳しくは scikit-learn の [Document](http://scikit-learn.org/stable/modules/cross_validation.html) などをご参照ください．

#ハイパーパラメータのチューニング
前節では，モデルの汎化性能を測定する方法をまとめました．
本節では，学習モデルに用いられるハイパーパラメータを調整し，モデルの汎化性能を向上させる方法について見ていきます．

最も用いられている方法は，グリッドサーチ（grid search）です．

これは指定したパラメータの全ての組み合わせに対して学習を行い，もっとも良い精度を示したパラメータを採用する方法です．

本記事では，RBF（radial basis function）を用いたカーネル法を用いたサポートベクターマシンを例にして考えてみます．

Scikit-learn ではこの手法は SVC() で実装されていますが，カーネルのバンド幅 gamma，正則化パラメータ C という 2 つの重要なパラメータが存在します．

このパラメータを変更することでモデルの性能は著しく変化するため，適切な値を発見することが非常に重要なタスクとなります．

gamma と C に対して，それぞれ 0.001, 0.01, 0.1, 1, 10, 100 を試してみることにすると，$6 \times 6 = 36$ 通りの組合せが存在することになります．

グリッドサーチでは，この 36 通りの組合せすべてについてモデルの学習と評価を行います．

##単純なグリッドサーチ
単純な方法は，gamma と C それぞれについて for ループを用いて実装することができます．

```python
from sklearn.svm import SVC

X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)

param_list = [0.001, 0.01, 0.1, 1, 10, 100]

best_score = 0
best_parameters = {}

for gamma in param_list:
    for C in param_list:
        svm = SVC(gamma=gamma, C=C)
        svm.fit(X_train, y_train)
        score = svm.score(X_test, y_test)
        # 最も良いスコアのパラメータとスコアを更新
        if score > best_score:
            best_score = score
            best_parameters = {'gamma' : gamma, 'C' : C}

print('Best score: {}'.format(best_score))
print('Best parameters: {}'.format(best_parameters))
```

出力結果

```
Best score: 0.9736842105263158
Best parameters: {'C': 100, 'gamma': 0.001}
```

グリッドサーチによってもっとも高いスコアとなったパラメータを得ることができました．

しかし，上記のコードには 1 つ問題が存在します．
それは，パラメータ選択の際に test set を使用してしまいっているという点です（score = svm.score(X_test, y_test) の部分）．

Test set は本来 training set を用いて学習が完了したモデルの汎化精度を評価するためのデータです．

ゆえに，training set に対してデータの分割を行い，パラメータ選択のためのデータセット validation set を新たに作成する必要があります．

```python
X_trainval, X_test, y_trainval, y_test = train_test_split(iris.data, iris.target, random_state=0)
X_train, X_valid, y_train, y_valid = train_test_split(X_trainval, y_trainval, random_state=1)
print('Size of trainings set: {}, validation set: {}, test set: {}'.format(X_train.shape, X_valid.shape, X_test.shape))
```

出力結果

```
Size of trainings set: (84, 4), validation set: (28, 4), test set: (38, 4)
```

Validation set を用いてグリッドサーチを行うコードは以下の通りです．

```python
best_score = 0
best_parameters = {}

for gamma in param_list:
    for C in param_list:
        svm = SVC(gamma=gamma, C=C)
        svm.fit(X_train, y_train)
        # validation set を用いて score を計算する
        score = svm.score(X_valid, y_valid)
        if score > best_score:
            best_score = score
            best_parameters = {'gamma' : gamma, 'C' : C}
    
svm = SVC(**best_parameters)
# best_parameters に対し，training set + validation set を用いて学習する
svm.fit(X_trainval, y_trainval)
# test set による評価は，best_parameters が得られて初めて行われる
test_score = svm.score(X_test, y_test)

print('Best score on validation set: {}'.format(best_score))
print('Best parameters: {}'.format(best_parameters))
print('Test set score with best parameters: {}'.format(test_score))
```

出力結果

```
Best score on validation set: 0.9642857142857143
Best parameters: {'C': 10, 'gamma': 0.001}
Test set score with best parameters: 0.9210526315789473
```

パラメータ選択に test set を用いていたときの Test set score は 0.97 程度でしたが，今回は 0.92 程度まで低下しており，best_parameters も異なる結果が得られたことがわかります．

##交差検証を用いたグリッドサーチ
単純なグリッドサーチでは，パラメータ選択の際に validation set を用いましたが，これは最初に実行した train_test_split() に依存しています．

より頑健な汎化性能の見積もりを行うために，グリッドサーチと交差検証を組合わせる手法が広く用いられています．
つまり，各パラメータの組合わせにおける評価値の計算の際に交差検証を行います．

この方法は次のコードにより実装することができます．

```python

best_score = 0
best_parameters  = {}

for gamma in param_list:
    for C in param_list:
        svm = SVC(gamma=gamma, C=C)
        # cross_val_score() による交差検証
        scores = cross_val_score(svm, X_trainval, y_trainval, cv=5)
        # k 個の評価値の平均を用いる
        score = np.mean(scores)
        if score > best_score:
            best_score = score
            best_parameters = {'gamma' : gamma, 'C' : C}

svm = SVC(**best_parameters)
# best_parameters に対し，training set + validation set を用いて学習する
svm.fit(X_trainval, y_trainval)
# test set による評価は，best_parameters が得られて初めて行われる
test_score = svm.score(X_test, y_test)

print('Best score on validation set: {}'.format(best_score))
print('Best parameters: {}'.format(best_parameters))
print('Test set score with best parameters: {}'.format(test_score))
```

出力結果

```
Best score on validation set: 0.9726896292113683
Best parameters: {'C': 100, 'gamma': 0.01}
Test set score with best parameters: 0.9736842105263158
```

交差検証を行わない場合と比較して，Test set score が 0.05 程度向上していることがわかりました．
したがって，交差検証とグリッドサーチを組み合わせて用いることで，未知のデータに対してより高い精度で予測できるモデルを学習できることがわかります．

また，scikit-learn　は交差検証を用いたグリッドサーチを実装した GridSearchCV クラスを提供しています．
GridSearchCV を用いることで，上記のコードを以下のように書き直すことができます．

```python
from sklearn.model_selection import GridSearchCV

# パラメータを dict 型で指定
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],  'gamma' : [0.001, 0.01, 0.1, 1, 10, 100]}

# validation set は GridSearchCV が自動で作成してくれるため，
# training set と test set の分割のみを実行すればよい
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)

grid_search = GridSearchCV(SVC(), param_grid, cv=5)

# fit 関数を呼ぶことで交差検証とグリッドサーチがどちらも実行される
grid_search.fit(X_train, y_train)
```

パラメータは dict 型で指定し，fit() 関数を呼ぶことで交差検証とグリッドサーチがどちらも実行されます．

最良のスコアとパラメータは自動的に best_score_，best_estimator_ 変数にそれぞれ格納されます．

```python
print('Test set score: {}'.format(grid_search.score(X_test, y_test)))
print('Best parameters: {}'.format(grid_search.best_params_))
print('Best cross-validation: {}'.format(grid_search.best_score_))
```

出力結果

```
Test set score: 0.9736842105263158
Best parameters: {'C': 100, 'gamma': 0.01}
Best cross-validation: 0.9732142857142857
```

ここで重要なのは，パラメータの選択（grid_search.fit(X_train, y_train) の部分）に test set を使用していないという点です．

GridSearchCV により，汎化精度が最も高くなるようなパラメータの発見が可能となります．


#まとめ
本記事では，

- 交差検証（Cross-validation）による汎化性能の評価
- グリッドサーチ（grid search）と呼ばれる方法でハイパーパラメータの調整

する方法についてまとめました．

交差検証により，汎化性能をより頑健な方法で評価することが可能となります．
また，グリッドサーチによりモデルの汎化性能を向上させることができました．

交差検証とグリッドサーチは scikit-learn の cross_val_score() と GridSearchCV を用いることでそれぞれ簡単に実装できることができます．

本記事に何か間違っている点や質問がある場合は，コメント等で知らせていただけたらと思います．


#参考
「Python ではじめる機械学習」オライリー・ジャパン、ISBN978-4-87311-798-0 [リンク](https://www.oreilly.co.jp/books/9784873117980/)