【Leave-one-out】データ量が少ない時に使うクロスバリデーション|python

こんにちは、青の統計学です。

今回はデータ量が少ない時に有効な交差検証法の一種、Leave-one-outCVを紹介いたします。

Leave one out CV

Leave-One-Out Cross-Validation (LOOCV) は、交差検証 (Cross-Validation) の一種であり、統計学や機械学習において、モデルの汎化性能を評価するために使用されます。

LOOCV は、非常にシンプルで効果的な手法であり、以下の手順で行われます。

1:データセットを一つずつ取り出して、テスト用データとして分割する。

2:残りのデータを全て訓練用データとして使い、モデルを訓練する。

3:テスト用データを使ってモデルを評価し、性能を記録する。

4:1-3の手順をデータセット内の全てのデータに対して繰り返す。

数学的背景

モデルを評価する指標として、MSEを使うとします。

回帰タスクですね。

$$MSE(θ;\hat{θ})=E[(\hat{θ}(X)-θ)^2]$$

$$MSE(θ;\hat{θ})=Var(\hat{θ})+Bias(θ)^2$$

これは、推定量と真の値θの差の平方の期待値です。推定量誤差のことです。

また、展開するとノイズと分散とバイアスの2乗和で表されることがわかります。

具体的な導出については以下のコンテンツをご覧ください。

【MSEを最小化】ガウス・マルコフの定理と最良線形不偏推定量について

回帰分析については、こちらをご覧ください。

さて、テストデータのMSEに関しては、データ数で平均をとります。

$$CV_{n}=\frac{1}{n} \sum_{i=1}^{n}MSE_{i}$$

例えば、\((x_{1},y_{1})\)だけテストデータに使い、\(MSE_{1}=(y_{1}-\hat{y}_{1})^2\)を計算します。

これを\((x_{2},y_{2})\)だけをテストデータに使う場合…とn回繰り返していき、n個のMSEを算出して平均をとります。

それが、モデルの「精度」だよね。という考えが、LOOCVの基本的なモチベーションになっています。

別に精度に使う指標が、MSEでなくても考え方は同じです。

LOOCV では、データセット内の全てのデータをテスト用データとして使用するため、非常に高い精度でモデルの汎化性能を評価できます。

しかし、データセットのサイズが大きい場合には計算時間が非常にかかってしまうというデメリットもあります。

k-fold交差検証法と何が違うのか

k-fold交差検証法は、データセットを3~10個ほどのサブセットに分けてテスト用データと教師用データを分けます。

モデルの学習回数つまり検証回数がデータ数n→3~10回くらいになるので、LOOCVよりも計算負荷はかかりません。

バイアスと分散のトレードオフについて

k-foldクロスバリデーションとLOOCV (Leave-One-Out Cross Validation) は、モデルの性能評価を行うためによく用いられる手法ですが、それぞれの方法は分散とバイアスの観点から異なる特性を持っています。

まず、分散の観点から見ると、k-foldクロスバリデーションはデータセットをk個の異なる部分集合に分割して、k回の評価を行うため、結果の分散が比較的小さくなる傾向があります。

一方、LOOCVは全てのデータを1つずつテストデータとして使用するため、分散が比較的大きくなります。

次にバイアスの観点に関して見ると、LOOCVはn-1個のデータを使って訓練したモデルを使うので、全てのデータに限りなく近い数のデータを訓練に使います。

そのため、バイアスは小さくなります。

k-foldクロスバリデーションに関しては\(\frac{(k-1)n}{k}\)の教師データしか使えないので、LOOCVよりはバイアスが大きくなります

バイアスというのは「異なるデータセットで学習したモデルの平均化した出力と真の出力の差」のことです。

LOOCVk-fold
biaslowhigh
variancehighlow

バイアスという観点だけではLOOCVはk-foldよりも明らかに優れていますが、計算量や分散の面で言えばk-foldの方が優れています。

もとのデータ量と相談して行ってください。

$$MSE(θ;\hat{θ})=Var(\hat{θ})+Bias(θ)^2$$

先ほど示した平均二乗誤差(MSE)は、ノイズと分散とバイアスの2乗和で表されることから、バイアスと分散がどちらかが大きすぎてもモデルとして望ましくないと言えます。

CODE|LOOCV

乳がんデータを使って、LOOCVを試します。

for文で、データ1個ずつに対してテストを行います。

モデルはロジスティック回帰です。

【分類タスク】ロジスティック回帰の使い方|python

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import LeaveOneOut
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# 乳がんデータを読み込む
data = load_breast_cancer()

# 特徴量とターゲットを分割する
X = data.data
y = data.target

# Leave-One-Out Cross-Validation を設定する
loocv = LeaveOneOut()

# ロジスティック回帰モデルを作成する
model = LogisticRegression()

# LOOCV を用いてモデルの性能を評価する
scores = []
for train_idx, test_idx in loocv.split(X):
    # テストデータと訓練データに分割する
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    # モデルを訓練する
    model.fit(X_train, y_train)
    
    # テストデータを用いてモデルを評価する
    y_pred = model.predict(X_test)
    score = accuracy_score(y_test, y_pred)
    
    # スコアを保存する
    scores.append(score)

# LOOCV での性能評価の平均値を計算する
mean_score = sum(scores) / len(scores)

# LOOCV での性能評価の標準偏差を計算する
std_score = np.std(scores)

# LOOCV での性能評価の平均値と標準偏差を出力する
print('LOOCV score:', mean_score)
print('LOOCV std:', std_score)

乳がんデータは500以上あるので、500回以上モデルの学習を行います。

かなりメモリを使って学習を行います。

下のように、標準偏差とスコア(正解率)が出ます。

LOOCV score: 0.9402460456942003
LOOCV std: 0.23703041840789138

正解率(accuracy)は、以下のような式です。

$$ACC(accuracy)=(\frac{TP+TN}{TP+TN+FP+FN})$$

悪性と診断良性と診断
悪性TPFN
良性FPTN
混同行列

CODE|k-fold (k=5)

比較として、k-foldの交差検証法を使ってスコアを見てみます。

今回はk=5とします。

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import numpy as np

# 乳がんデータを読み込む
data = load_breast_cancer()

# 特徴量とターゲットを分割する
X = data.data
y = data.target

# k-fold クロスバリデーション を設定する
k = 5
kf = KFold(n_splits=k, shuffle=True, random_state=42)

# ロジスティック回帰モデルを作成する
model = LogisticRegression()

# k-fold クロスバリデーション を用いてモデルの性能を評価する
scores = []
for train_idx, test_idx in kf.split(X):
    # テストデータと訓練データに分割する
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    # モデルを訓練する
    model.fit(X_train, y_train)
    
    # テストデータを用いてモデルを評価する
    y_pred = model.predict(X_test)
    score = accuracy_score(y_test, y_pred)
    
    # スコアを保存する
    scores.append(score)

# k-fold クロスバリデーション での性能評価の平均値を計算する
mean_score = sum(scores) / len(scores)

# k-fold クロスバリデーション での性能評価の標準偏差を計算する
std_score = np.std(scores)

# k-fold クロスバリデーション での性能評価の平均値と標準偏差を出力する
print('k-fold score:', mean_score)
print('k-fold std:', std_score)

LOOCVに比べると驚くほど早く処理が終わったと思います。

k-fold score: 0.9402111473373699
k-fold std: 0.02913430265305988

LOOCVの方が、標準誤差が減りましたね。

標準誤差とは、以下のように標準偏差をデータ数の平方根で割ったものでした。

$$SE = \frac{SD}{\sqrt{n}}$$

これは先ほどの数学的背景でご説明した通り、LOOCVはk-foldよりも分散が大きいという結果になっておりますね。

まとめ

LOOCV は、主に回帰問題や分類問題において、モデルの汎化性能を評価するために使用されます。

LOOCV を使用することで、モデルが過学習しているかどうかを確認することができ、過学習している場合には、モデルの複雑さを調整することで改善することができます。

過学習の話で余談ですが、パラメータが増えるとテスト誤差は大きくなるというのが一般的だったのが、Transformerを使ったLLMにはスケール則という、データやパラメータ、計算資源を増やせばテスト誤差が下がるという話があり、この辺りの原理がまだまだわかっていません。

我々の脳の神経細胞も2000億くらいニューロンがあり、1個のニューロンも1万個くらいのシナプスによって構成されており、モデルで例えるととても大きいです。LLMは我々の知能に近づいてきたということでしょうか、非常に興味深いです。

機械学習については、以下のコンテンツをご覧ください。

【機械学習】決定木の仕組みと実装方法について|python

【Sequential】Kerasを使ったニューラルネットワーク|python

【ランダムフォレスト】ブートストラップ法を決定木に応用|python

【機械学習】単回帰分析をpythonで実装してみましょう

【判別問題】サポートベクトルマシン(SVM)の仕組み|python

【kaggle】ベイズ最適化とXGBでtitanicの予測問題を解く|python

FOLLOW ME !