
Cross-Validation(交差検証)って何者?
はじめに
こんにちは!ハルです.今まで,データ分析コンペティションに何度か参加してきましたが,その都度,困っていた検証用データの分け方について説明していこうと思います!自分自身も素人なので間違っているところがあればぜひ教えてください!
参考文献
下記の本およびscikit-learnのドキュメントを参考にしています.
https://scikit-learn.org/stable/api/sklearn.model_selection.html
Cross-Validationとは?
交差検証とは,機械学習モデルの汎化能力を正確に測定するために用いられる手法の1つです.通常,データセットを訓練用データとテスト用データに分割する,Hold-out法によって,モデルを評価しますが,この方法ではデータの分割方法によって評価結果が大きく異なる恐れがあります.交差検証は,この問題を解決するためにデータを複数の分割に分け,それぞれの分割に対して訓練とテストを繰り返す手法になっています.
具体的には,データセットを複数の等しいサイズの部分(fold)に分け,1つの部分をテストデータとして使用し,残りの部分を訓練データとしてモデルを構築します.このプロセスを,各部分を一度ずつテストデータとして使うまで繰り返し,すべての結果を平均することでモデルの評価指標を算出します.最も一般的な方法はk分割交差検証(k-fold cross-validation)であり,このkはデータセットをいくつの部分に分割するかを指定するパラメータになります.
交差検証の大きな利点は,データセットの分割方法に依存せず,すべてのデータが一度はテストデータとして使用されるため,評価結果のバラつきを抑え,モデルの真の性能をより正確に把握できることです.また、過学習を防ぐための一つの有効な手段でもあり,モデルが訓練データに過度に適合してしまうのを避け,新しいデータに対しても安定したパフォーマンスを発揮できるかどうかを確認することができます.
本記事では,様々な交差検証について自身の勉強も含めて,解説&まとめていきます!
今回使うデータの紹介
今回は主にscikit-learnで使うことのできるデータセットIrisを使って各交差検証の手法の例を示していこうと思います.
# 必要なパッケージのインポート
from sklearn.datasets import load_iris
import pandas as pd
# データセットをロード
iris = load_iris()
# データフレームの作成
# 説明変数のデータフレーム
iris_tr = pd.DataFrame(iris.data, columns=iris.feature_names)
iris_tr
# 目的変数のデータフレーム
iris_target = pd.DataFrame(iris.target, columns=['target'])
iris_target
# データフレームの結合
data = pd.concat([iris_tr, iris_target], axis=1)
上記のコードでは,scikit-learnを使うことによってIrisデータセットを読み込んだ後に,データフレームに変換しています.
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 7))
ax = fig.add_subplot(111)
# 横棒グラフの作成(heightを小さく設定して細い横棒にする)
for i in range(data.shape[0]):
if data['target'][i] == 0:
ax.barh(0.5, 1, left=i, color='r', height=0.2)
elif data['target'][i] == 1:
ax.barh(0.5, 1, left=i, color='g', height=0.2)
elif data['target'][i] == 2:
ax.barh(0.5, 1, left=i, color='b', height=0.2)
# 凡例の作成
target1_patch = mpatches.Patch(color='r', label='Target1')
target2_patch = mpatches.Patch(color='g', label='Target2')
target3_patch = mpatches.Patch(color='b', label='Target3')
# 凡例の表示
ax.legend(handles=[target1_patch, target2_patch, target3_patch], loc='upper right')
# グラフの設定
ax.set_yticks([])
ax.set_ylim(0, 1)
ax.set_xticks(np.arange(0, data.shape[0] + 1, 50))
plt.xlabel("Sample Index")
plt.title("Iris Data")
plt.show()
上記はIrisデータセットを可視化するサンプルコードになります.特徴量などは今回は扱わないので詳細の説明は省略させていただきます.このIrisデータセットは50個ずつの3つのクラスを持つデータセットで,図で見ると下記のような感じになります.では説明に入っていきます!!

K-Fold Cross-Validation(k分割交差検証)
最初にK-fold Cross-Validationについて説明していきます.先ほども説明があったように,k分割交差検証はデータセットをk個に同じ大きさでわけ,1つの部分をテストデータとして使用し,残りの部分を訓練データとしてモデルを学習します.Irisデータを用いたときにK-Fold Cross-Validationを行うためのサンプルコードとその結果を示します.
# k分割交差検証を行うためのパッケージをインポート
from sklearn.model_selection import KFold
import pandas as pd
import numpy as np
# k分割交差検証のインスタンスを生成
kf = KFold(n_splits=5)
# プロットのための準備
fig, ax = plt.subplots(figsize=(10, 7))
# 各分割をループして可視化
for i, (train_index, test_index) in enumerate(kf.split(data)):
# 訓練データ部分を青色で描画
for idx in train_index:
ax.barh(i, 1, left=idx, color='#005AFF', height=0.8)
# テストデータ部分を赤色で描画
for idx in test_index:
ax.barh(i, 1, left=idx, color='#FF4B00', height=0.8)
# グラフの設定
ax.set_yticks(np.arange(kf.n_splits))
ax.set_yticklabels([f'Fold {i+1}' for i in range(kf.n_splits)])
ax.set_xlim(0, data.shape[0])
# 凡例の作成
train_patch = mpatches.Patch(color='#005AFF', label='Train')
test_patch = mpatches.Patch(color='#FF4B00', label='Test')
# 凡例の表示
ax.legend(handles=[train_patch, test_patch], loc='upper right')
# グラフの設定
plt.xlabel("Sample Index")
plt.ylabel("Folds")
plt.title("K-Fold Cross Validation Split Visualization")
plt.tight_layout()
plt.show()

上記の例では,0~29などまとまった部分がテストデータになっていますが,サンプルコードのインスタンス生成部分の引数を変更することによって,下図のように分割場所をバラバラにすることができるようになります.もし,データ分析コンペなどに用いる場合には再現性のためにもシード値は固定した方が良いと思います.
# k分割交差検証のインスタンスを生成
kf = KFold(n_splits=5, shuffle=True, random_state=0)

Stratified K-Fold(層化抽出 k分割交差検証)
Stratified K-Foldとは分類タスクにおいて,foldごとに含まれる目的変数の割合を等しくする交差検証法になっています.テストデータに含まれる各クラスの割合と,学習データに含まれる各クラスタの割合はほとんど同じだろうという仮定に基づく手法です.また,K-fold Cross-Validationではデータセットをk個にただ分割するだけなので,あるFoldでは3つのクラスがある中1つのクラスが含まれないようなFoldがある場合が出て来てしまいます.特に多クラス分類ではデータを分割したときに,あるクラスが全く含まれないとは言わずとも,割合に偏りが出てしまう恐れがあるので,それを解決する手法でもあります.下記にサンプルコードとその結果を示します.
# 層化抽出k分割交差検証を行うためのパッケージをインポート
from sklearn.model_selection import StratifiedKFold
import matplotlib.patches as mpatches
# 説明変数
X = data.drop('target', axis=1)
# 目的変数
y = data['target']
# インスタンスの生成
stf = StratifiedKFold(n_splits=5)
# プロットのための準備
fig, ax = plt.subplots(figsize=(10, 7))
for i, (train_index, test_index) in enumerate(stf.split(X, y)):
for j in range(data.shape[0]):
if data['target'][j] == 0:
ax.barh(i, 1, left=j, color='#005AFF', label="Target1")
elif data['target'][j] == 1:
ax.barh(i, 1, left=j, color='#FF4B00', label="Target2")
elif data['target'][j] == 2:
ax.barh(i, 1, left=j, color='g', label="Target2")
# テストデータ部分を斜線で描画
for idx in test_index:
if data['target'][idx] == 0:
ax.barh(i, 1, left=idx, color='#005AFF', height=0.8, hatch="//", label="Test" if i == 0 and idx == test_index[0] else "")
elif data['target'][idx] == 1:
ax.barh(i, 1, left=idx, color='#FF4B00', hatch="//", height=0.8)
elif data['target'][idx] == 2:
ax.barh(i, 1, left=idx, color='g', hatch="//")
# グラフの設定
ax.set_yticks(np.arange(kf.n_splits))
ax.set_yticklabels([f'Fold {i+1}' for i in range(kf.n_splits)])
ax.set_xlim(0, data.shape[0])
# 凡例の作成(斜線付きのテストデータも含む)
group1_patch = mpatches.Patch(color='#005AFF', label='Target1')
group2_patch = mpatches.Patch(color='#FF4B00', label='Target2')
group3_patch = mpatches.Patch(color='g', label='Target3')
test_patch = mpatches.Patch(facecolor='white', edgecolor='black', hatch='//', label='Test')
# 凡例を追加
ax.legend(handles=[group1_patch, group2_patch, group3_patch, test_patch], loc='upper right')
plt.xlabel("Sample Index")
plt.ylabel("Folds")
plt.title("Stratified K-Fold Cross Validation Split Visualization")
plt.tight_layout()
plt.show()

上記の図では少しわかりにくいですが,それぞれの色が各クラスを表していて,斜線部分が各分割でのテストデータを表しています.テストデータには同じ割合で各クラスからデータが抽出されていることがわかります!また,K分割交差検証の時と同様に,引数にshuffleが設定されているのでshuffle=True として,実行した結果は以下になります.

Leave-One-Out
Leave-one-out交差検証(LOO)は,特に小規模なデータセットを扱う際に広く使用される評価手法です.LOOでは,データセットがデータポイントの数だけ分割されます.各分割ごとに,1つのデータが検証用データセットとして使われ,残りのデータポイントは訓練用データセットとして使用されます.このプロセスは、すべてのデータポイントが一度ずつ検証用データセットとして使われるまで繰り返されます.
LOOの利点は,各イテレーションで最大限のデータを訓練に使用できる点ですが,大規模なデータセットでは計算コストが高くなる可能性があります.この手法は,モデルがすべてのデータポイントでテストされるため,汎化性能を評価するのに役立ちますが,k分割交差検証などの他の手法に比べて計算時間が長くなる傾向があります.また,データ数が多いデータセットではK-Fold Cross-Validationでも十分なので,主にデータ数が少ないようなデータセットに用いる手法です.以下にサンプルコードとその結果を示します.
# leave-one-out交差検証を行うためのパッケージをインポート
from sklearn.model_selection import LeaveOneOut
# プロットのための準備
fig, ax = plt.subplots(figsize=(10, 7))
loo = LeaveOneOut()
for i, (train_index, test_index) in enumerate(loo.split(data)):
# 訓練データ部分を青色で描画
for idx in train_index:
ax.barh(i, 1, left=idx, color='#005AFF', height=0.8)
# テストデータ部分を赤色で描画
for idx in test_index:
ax.barh(i, 1, left=idx, color='#FF4B00', height=0.8)
# グラフの設定
ax.set_yticks(np.arange(data.shape[0]))
ax.set_xlim(0, data.shape[0])
# 凡例の作成
train_patch = mpatches.Patch(color="#005AFF", label="Train")
test_patch = mpatches.Patch(color="#FF4B00", label="Test")
ax.legend(handles=[train_patch, test_patch], loc="upper right")
plt.xlabel("Sample Index")
plt.ylabel("Folds")
plt.title("Leave-One-Out Cross Validation Split Visualization")
plt.tight_layout()
plt.show()

Group K-Fold
**Group K-Fold Cross-Validation(**グループK分割交差検証)は,データセットをグループ単位で分割し,交差検証を行う手法です.一般的な交差検証とは異なり,Group K-Foldでは,データの個別のインスタンスではなく,関連するグループを考慮して分割します.これにより,特定のグループが同時に訓練とテストデータに含まれないようにし,過度な依存関係を防ぎます.
この手法は,データに「グループ」が含まれている場合や,各サンプルが互いに関連している場合に特に有用です.グループごとの相関が強いデータを使用してモデルを評価すると,過度に楽観的な結果を引き起こす可能性があるため,Group K-Foldは信頼性の高い評価を行うために必要です.
Group K-Foldでは,Irisデータセットとは別に人工データを作成して,見本を作ってみました.(よく考えたら,目的変数をグループとしたら良かったのか??)
# Group K-Fold用のデータセットの生成
n_samples = 99 # サンプル数
n_groups = 3 # グループ数
# 特徴量
X = np.random.randn(n_samples, 2)
# ターゲット(ラベル)
y = np.random.randint(0, 2, size=n_samples)
# グループ(各グループに10サンプルずつ)
groups = np.repeat(np.arange(n_groups), n_samples // n_groups)
# データフレームとして保存
data = pd.DataFrame(X, columns=["Feature1", "Feature2"])
data['Target'] = y
data['Group'] = groups
上記のコードによって作成した人工データをGroup K-Foldによって分割した時のサンプルコードと結果を示します.
# Group K-Foldを行うためのパッケージをインポート
from sklearn.model_selection import GroupKFold
import matplotlib.patches as mpatches
# Group K-Foldのインスタンスを生成
gkf = GroupKFold(n_splits=3)
# プロットのための準備
fig, ax = plt.subplots(figsize=(10, 7))
# 各分割をループして可視化
for i, (train_index, test_index) in enumerate(gkf.split(X, y, groups)):
for j in range(data.shape[0]):
if data['Group'][j] == 0:
ax.barh(i, 1, left=j, color='#005AFF', height=0.5, label="Group1" if i == 0 and j == 0 else "")
elif data['Group'][j] == 1:
ax.barh(i, 1, left=j, color='#FF4B00', height=0.5, label="Group2" if i == 0 and j == 33 else "")
elif data['Group'][j] == 2:
ax.barh(i, 1, left=j, color='g', height=0.5, label="Group3" if i == 0 and j == 66 else "")
# # 訓練データ部分を青色で描画
# for idx in train_index:
# ax.barh(i, 1, left=idx, color='#005AFF', height=0.8, label="Train" if i == 0 and idx == train_index[0] else "")
# テストデータ部分を赤色で描画
for idx in test_index:
if data['Group'][idx] == 0:
ax.barh(i, 1, left=idx, height=0.5, color='#005AFF', hatch="//", label="Test" if i == 0 and idx == test_index[0] else "")
elif data['Group'][idx] == 1:
ax.barh(i, 1, left=idx, height=0.5, color='#FF4B00', hatch="//")
elif data['Group'][idx] == 2:
ax.barh(i, 1, left=idx, height=0.5, color='g', hatch="//")
# グラフの設定
ax.set_yticks(np.arange(3))
ax.set_yticklabels([f'Fold {i+1}' for i in range(gkf.get_n_splits())])
ax.set_xlim(0, data.shape[0])
# 凡例の作成(斜線付きのテストデータも含む)
group1_patch = mpatches.Patch(color='#005AFF', label='Group1')
group2_patch = mpatches.Patch(color='#FF4B00', label='Group2')
group3_patch = mpatches.Patch(color='g', label='Group3')
test_patch = mpatches.Patch(facecolor='white', edgecolor='black', hatch='//', label='Test')
# 凡例を追加
ax.legend(handles=[group1_patch, group2_patch, group3_patch, test_patch], loc='upper right')
# グラフの設定
plt.xlabel("Sample Index")
plt.ylabel("Folds")
plt.title("Group K-Fold Cross Validation Split Visualization")
plt.tight_layout()
plt.show()

各分割において,訓練データとテストデータに同じグループが含まれないようになっています.このようにして,グループ内の依存関係を排除しながらモデルの検証を行うことができます.またGroup K-Foldのみshuffleが引数として用意されていないので,参考文献ではK-Foldを用いて実装していました.
最後に
今回は交差検証についてまとめてみました!各手法の特徴について,可視化しながら説明できたかなと思います.もしわかりにくいところや間違っているところがあればどんどんコメントしてください!
現在はDBスペシャリストの勉強に励んでいるので,データベースやデータ基盤の話が書けたら!と思っています.ご覧いただきありがとうございました!