Cross Validationを学ぶ|K-FoldとStratified K-FoldをTitanicデータで比較

今回は、Titanicデータセットを使った機械学習の学習で、Cross Validation(交差検証)について学んだ。

これまでは、学習データとテストデータに分けてモデルの精度を確認してきた。今回はそこから一歩進み、学習データをさらに複数に分割して、モデルを複数回評価する方法を学習した。

さらに、Titanicのような分類問題では、単純なK-FoldよりもStratified K-Foldが適していることも確認した。

これまでのモデル評価

これまでのTitanicでは、まずデータを学習用とテスト用に分割していた。

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42
)

全体の80%を学習データ、20%をテストデータとして使う方法である。

そして、学習データでモデルを作り、テストデータに対するAccuracyを計算していた。

この方法でもモデルの評価はできるが、ある1回のデータ分割だけを使って評価しているという問題がある。

Cross Validationとは

そこで登場するのがCross Validation(交差検証)である。

Cross Validationでは、データを複数のグループに分割し、学習とValidationを何度も繰り返す。

例えば5分割する場合、次のようになる。

全データ
┌────┬────┬────┬────┬────┐
│ A  │ B  │ C  │ D  │ E  │
└────┴────┴────┴────┴────┘

1回目
Train: B C D E
Validation: A

2回目
Train: A C D E
Validation: B

3回目
Train: A B D E
Validation: C

4回目
Train: A B C E
Validation: D

5回目
Train: A B C D
Validation: E

この方法では、全データが一度ずつValidationとして使われる

そして、それぞれのValidationで得られたスコアを平均する。

1回目 → 0.81
2回目 → 0.83
3回目 → 0.79
4回目 → 0.82
5回目 → 0.84

平均 → 0.818

これによって、特定の1つのValidationデータだけに依存するのではなく、データ全体に対してモデルがどの程度安定して性能を発揮するかを確認できる。

なぜ全データを一度ずつValidationにするのか

ここは最初に少し疑問に感じたところである。

「4つを学習データ、1つをテストデータとして、それを5パターン行って平均すればよいのではないか」と考えた。

実際、その考え方でほぼ合っている。

ただし、Cross Validationでは通常、この1つをテストデータではなくValidationデータと呼ぶ。

最終的なテストデータは別に残しておくためである。

例えば、最初にデータを次のように分ける。

全データ
├── 80% → Cross Validationに使用
└── 20% → 最終Testデータ

80%のデータについてCross Validationを行い、モデルやパラメータを決める。

そして最後に、今まで触っていなかった20%のTestデータで、本当の未知データに対する性能を確認する。

scikit-learnでCross Validationを実行する

scikit-learnではcross_val_score()を使うと、Cross Validationを簡単に実行できる。

from sklearn.model_selection import cross_val_score

scores = cross_val_score(
model,
X,
y,
cv=5,
scoring="accuracy"
)

print(scores)
print(scores.mean())

cv=5は5分割するという意味である。

実行すると5つのAccuracyが得られ、その平均を取ることでCross Validation全体のスコアを確認できる。

K-Fold Cross Validation

cv=5で指定していたものを明示的に書くと、KFoldを使える。

from sklearn.model_selection import KFold

kFold = KFold(
n_splits=5,
shuffle=True,
random_state=42
)

n_splits=5は5分割を意味する。

random_state=42については、42という数字そのものに特別な意味はない。シャッフル結果を再現できるようにするための値である。

shuffle=Trueとは何か

今回、shuffle=Trueについて少し理解に時間がかかった。

shuffle=Trueは、データを分割する前にランダムに並び替える設定である。

例えば、データが次の順番で並んでいたとする。

1 2 3 4 5 6 7 8 9 10

シャッフルすると、例えば次のようになる。

7 2 9 4 1 8 5 10 3 6

そして、この状態から5分割する。

ここで重要なのは、shuffle=Trueそのものには目的変数の割合を揃える機能はないという点である。

あくまで「元のデータの並び順による偏りを避けるためにシャッフルする」機能である。

TitanicではSurvivedの割合が重要

今回扱っているTitanicは分類問題であり、目的変数はSurvivedである。

Survived
0 → 死亡
1 → 生存

Titanic全体では、死亡者と生存者の人数に偏りがある。

このようなデータを単純にKFoldで分割すると、分割の仕方によっては各Foldの生存者割合が大きく異なる可能性がある。

例えば極端なケースでは、

Fold 1 → 生存者10%
Fold 2 → 生存者50%
Fold 3 → 生存者30%
Fold 4 → 生存者40%
Fold 5 → 生存者60%

ということも考えられる。

この状態では、Foldごとに評価条件が違いすぎる。

Stratified K-Fold

そこで登場するのがStratified K-Foldである。

Stratified K-Foldは、目的変数の割合をできるだけ維持した状態でデータを分割する

例えば全体の生存率が約38%なら、各Foldでも生存率が約38%になるように分割する。

全体
Survived = 0 → 約61.6%
Survived = 1 → 約38.4%

```
    ↓
```

Fold 1 → 0:約61.6%、1:約38.4%
Fold 2 → 0:約61.6%、1:約38.4%
Fold 3 → 0:約61.6%、1:約38.4%
Fold 4 → 0:約61.6%、1:約38.4%
Fold 5 → 0:約61.6%、1:約38.4%

完全に同じ割合にできるとは限らないが、各Foldでできるだけ近い割合になるように分割される。

Stratified K-Foldをコードで指定する

scikit-learnでは次のように書く。

from sklearn.model_selection import StratifiedKFold

stratifiedKFold = StratifiedKFold(
n_splits=5,
shuffle=True,
random_state=42
)

そして、cross_val_score()cvに渡す。

stratifiedKFold_scores = cross_val_score(
    model,
    X,
    y,
    cv=stratifiedKFold,
    scoring="accuracy"
)

print(stratifiedKFold_scores)
print(stratifiedKFold_scores.mean())

ここで重要なのは、StratifiedKFoldyの値を見て分割方法を決めることである。

X → モデルが学習する特徴量

y → StratifiedKFoldが確認する目的変数
↓
0 / 1
↓
各Foldの割合が近くなるように分割

KFoldとStratified K-Foldを実際に比較する

実際にTitanicのモデルを使って比較してみた。

KFold

kFold = KFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

kFold_scores = cross_val_score(
model,
X,
y,
cv=kFold,
scoring="accuracy"
)

Stratified K-Fold

stratifiedKFold = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

stratifiedKFold_scores = cross_val_score(
model,
X,
y,
cv=stratifiedKFold,
scoring="accuracy"
)

実行結果は次の通りだった。

KFoldの結果

[0.80446927 0.78651685 0.83707865 0.7752809  0.78651685]

0.7979725064339966

平均Accuracyは約0.798である。

Stratified K-Foldの結果

[0.7877095  0.80337079 0.78651685 0.78651685 0.82022472]

0.7968677421379701

平均Accuracyは約0.797だった。

思ったほど差が出なかった

今回の結果では、KFoldとStratified K-Foldの平均スコアにほとんど差がなかった。

KFold
平均:0.7980

Stratified K-Fold
平均:0.7969

ばらつきについても、大きな違いは見られなかった。

最初は「Stratified K-Foldを使えば明確にスコアが良くなる」と考えていたが、そういうものではないことが分かった。

Stratified K-Foldの目的は、スコアを必ず上げることではない

目的は、目的変数の割合がFoldごとに大きく偏らないようにして、評価条件をなるべく揃えることである。

なぜ今回KFoldでも問題がなかったのか

今回のKFoldでは、

shuffle=True
random_state=42

としている。

そのため、データをシャッフルした結果、たまたま各FoldのSurvivedの割合がそれほど大きく偏らなかった可能性がある。

つまり、今回については、

「KFoldでもたまたま比較的バランスのよい分割になった」

と考えられる。

ただし、これは「KFoldなら必ず問題ない」という意味ではない。

別のデータセットや別のrandom_stateでは、目的変数の割合が大きく偏る可能性がある。

Stratified K-Foldのメリット

Stratified K-Foldを使えば、単にランダムに分割して「結果的に割合が揃った」という状態ではなく、目的変数の割合を維持することを意識して分割できる

そのため、分類問題ではStratified K-Foldを使うのが一般的である。

特に、目的変数が不均衡なデータでは重要になる。

例えば、

0 → 95%
1 → 5%

というデータを普通のKFoldで分割すると、Foldによっては1が極端に少なくなる可能性がある。

そのようなデータでは、Stratified K-Foldによって各Foldの割合を保つメリットが大きい。

各Foldの生存率を確認する

Stratified K-Foldが本当にSurvivedの割合を揃えているのかを確認するには、モデルを使わずにFoldそのものを確認すると分かりやすい。

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

for fold, (train_idx, valid_idx) in enumerate(cv.split(X, y)):
print(f"Fold {fold + 1}")

```
print("train:", y.iloc[train_idx].mean())
print("valid:", y.iloc[valid_idx].mean())
```

TitanicのSurvivedは0または1なので、平均を計算するとそのまま生存率になる。

例えば、

0
0
1
0
1

なら、平均は0.4であり、生存率40%という意味になる。

この値を各Foldについて確認すれば、Stratified K-Foldによって目的変数の割合が維持されていることを確認できる。

今回の学習で分かったこと

今回の学習では、まずCross Validationの目的を改めて整理した。

単純なTrain/Test分割では、1回のデータ分割に対する評価になってしまう。一方、K-Foldでは複数の分割を使って評価するため、特定のValidationデータだけに依存しにくくなる。

さらに、K-Foldでは単純に分割するだけなので、分類問題では目的変数の割合が偏る可能性がある。

そこでStratified K-Foldを使い、yの割合をできるだけ維持した状態で分割する。

今回の学習を進める中で、ChatGPTを講師兼壁打ち相手として使った。

特に、

  • なぜ全データが一度ずつValidationになるのか
  • shuffle=Trueは何をしているのか
  • Stratified K-Foldはどうやって割合を揃えているのか

といった部分について、疑問に感じたところを一つずつ確認した。

その結果、最初は混同していた「シャッフルすること」と「目的変数の割合を維持すること」は別の処理だと整理できた。

まとめ

  • Cross Validationは、複数のデータ分割を使ってモデルを評価する方法である
  • K-FoldではデータをK個に分割し、それぞれを順番にValidationとして利用する
  • 全データが一度ずつValidationになるため、特定のデータだけに依存しにくい
  • shuffle=Trueは分割前にデータをシャッフルする
  • Stratified K-Foldは目的変数の割合をできるだけ維持して分割する
  • 分類問題ではStratified K-Foldが適している
  • 今回のTitanicデータではKFoldとStratified K-Foldの平均スコアに大きな差はなかった
  • Stratified K-Foldは「スコアを上げるため」ではなく、「評価データの構成をなるべく揃えるため」に使う

今回の学習を通して、モデルの性能だけでなく、どのようにデータを分割して評価するかも機械学習では重要であることが分かった。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA