機械学習における学習の評価手法についてまとめ

# 記事のきっかけ
今迄分析コンペなどに出た時に、データの評価は何となくでコードをコピペしながらやっていたがきちんと理解して使えてたわけではないな、と思い改めて整理することにしました。

# 評価の目的
そもそも評価（分析コンペの文脈で）を行う目的とは、、、
1. モデル改善の指針となるスコアを示す
2. テストデータに対するスコアやばらつきを見積もる

1は訓練データで学習がきちんとできているかの確認し、
2はテストデータで推論をおこなった時、うまく結果が出せるかを確認するイメージ

# 評価までの流れ
データを準備して、モデルの評価をするまでの流れは以下のイメージ

```mermaid
---
config:
  theme: redux
  look: handDrawn
  layout: elk
---
flowchart TD
 subgraph s1["訓練データ"]
        n1["データ分割"]
        n5["学習"]
        n6["評価"]
  end
 subgraph s2["テストデータ"]
        n7["推論"]
  end
    n5 --> n6
    n1 --> n5
    n2["データ準備"] --> s1
    s1 --> s2
    n1@{ shape: rect}
    n2@{ shape: rect}
```

# データ分割手法

以下に、主要なデータ分割手法の概要、主要ライブラリ、およびそれぞれの手法が有効なケースと苦手なケースをまとめた表を示します。

| 手法名 | 概要 | 有効なケース | 苦手なケース |
|---|---|---|---|
| **Hold-out法** | データを学習用と評価用（テスト用）の2つに分割する最もシンプルな方法。通常、学習用データでモデルを訓練し、評価用データで性能を評価する。 | 大規模なデータセット、計算リソースが限られている場合、モデルの初期評価。 | データセットが小さい場合（分割によってデータが偏る可能性）、評価結果がデータ分割に依存しやすい。 |
| **K分割交差検証 <br>(K-Fold Cross-Validation)** | データをK個のサブセットに分割し、K回学習と評価を繰り返す。各フォールドが1回ずつ評価用データとして使用され、残りのK-1個のフォールドが学習用データとして使用される。最終的な評価はK回の評価結果の平均として算出される。 | データセットが中規模から大規模、よりロバストなモデル評価が必要な場合、データセット全体を評価に活用したい場合。 | 計算コストが高い（K回モデルの訓練が必要）、時系列データには不向き。 |
| **層化K分割交差検証<br> (Stratified K-Fold Cross-Validation)** | K分割交差検証の改良版。特に分類問題において、各フォールド内のクラスの比率が元のデータセットのクラス比率と等しくなるように分割する。 |  クラスの分布が不均衡な分類問題。 | 回帰問題には適用できない。 |
| **Leave-One-Out Cross-Validation (LOOCV)** | K分割交差検証の特殊なケースで、Kをデータセットのサンプル数Nとする。N回の学習と評価を繰り返し、各学習では1つのサンプルが評価用、残りのN-1個が学習用として使用される。 |  非常に小さいデータセット、モデルのバイアスを最小限に抑えたい場合。 | 計算コストが非常に高い（データ数N回モデルの訓練が必要）、大規模なデータセットでは非現実的。 |
|**グループ交差検証 (Group K-Fold Cross-Validation)** | 特定のグループ（例：患者ID、場所）に属するデータが、学習用と評価用で混在しないように分割する。同じグループのデータは必ず同じセット（学習用または評価用）に属する。 | 患者ごとに複数のデータがある医療データ、ユーザーごとに複数の行動ログがあるデータなど、グループ間の独立性を保ちたい場合。 | グループの定義が難しい場合、グループ数が少ない場合。 |
|

# それぞれの実装例
## Hold-out法
```python
from sklearn.model_selection import train_test_split
# データを訓練用とテスト用に分割 (例: 訓練用80%, テスト用20%)
# データセットのXとyを使用します
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
```
## K分割交差検証
```python
from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score
import numpy as np
# KFoldオブジェクトの作成 (例: K=5)
kf = KFold(n_splits=5, shuffle=True, random_state=42)
accuracies = []
for fold, (train_index, test_index) in enumerate(kf.split(X)):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    model = Model()＃仮
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    accuracies.append(accuracy)
    print(f"  Fold {fold+1} 精度: {accuracy:.4f}")
print(f"平均精度: {np.mean(accuracies):.4f}")
```

## 層化K分割交差検証
```python
from sklearn.model_selection import StratifiedKFold

# StratifiedKFoldオブジェクトの作成 (例: K=5)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

accuracies = []
for fold, (train_index, test_index) in enumerate(skf.split(X, y)):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    model = Model()＃仮
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    accuracies.append(accuracy)
    print(f"  Fold {fold+1} 精度: {accuracy:.4f} (テストデータ クラス分布: {np.bincount(y_test)})")

print(f"平均精度: {np.mean(accuracies):.4f}")
```

## Leave-One-Out Cross-Validation
```python
from sklearn.model_selection import LeaveOneOut
from sklearn.metrics import accuracy_score
import numpy as np
# LeaveOneOutオブジェクトの作成
loo = LeaveOneOut()
accuracies = []
for fold, (train_index, test_index) in enumerate(loo.split(X)):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    model = Model()
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    accuracies.append(accuracy)
print(f"平均精度: {np.mean(accuracies):.4f}")
```

## グループ交差検証
```python
from sklearn.model_selection import GroupKFold
from sklearn.metrics import accuracy_score
import numpy as np

# GroupKFoldオブジェクトの作成 (例: K=5)
gkf = GroupKFold(n_splits=5)
#groups はデータが所属するグループのID

accuracies = []
for fold, (train_index, test_index) in enumerate(gkf.split(X, y, groups=groups)):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    groups_train, groups_test = groups[train_index], groups[test_index] # グループ情報も確認のため取得
    # テストセットのグループが訓練セットに含まれていないことを確認
    assert len(set(groups_test) & set(groups_train)) == 0, "テストグループが訓練グループに含まれています！"
    model = Model()
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    accuracies.append(accuracy)
    print(f"  Fold {fold+1} 精度: {accuracy:.4f}")

print(f"平均精度: {np.mean(accuracies):.4f}")

```

# 時系列データに関する扱いのメモ
- 基本、validデータはtrainデータより後の時刻のデータとする
- データの性質によっては普通の交差検証で評価すルことも可能
- hold-out・・・テストデータに近いデータをvaidデータとする
- 交差検証・・・データを減らしていく or 時間をずらす

# まとめ
評価におけるデータ分割の方法についてまとめました。
自分自身、時系列データなどは具体的な例を使って理解できていないので、これから掘り下げていきたいと思います。
大切なことは、評価におけるデータ分割では学習データとテストデータの分割をまねる必要があるということでした。今回の記事作成によって、手元のデータで訓練データ・学習データの状況を再現するための技術なのだという理解は深まった気がしています。

# 参考資料
- [Kaggleで勝つデータ分析の技術](https://gihyo.jp/book/2019/978-4-297-10843-4)