kaggleでよく使う交差検証テンプレ(Keras向け)

はじめに

Zennで投稿した「kaggleでよく使う交差検証テンプレ(Keras向け)」に追記した内容になります。

kaggleでクロスバリデーションをする際に、毎回調べては、毎回少しずつ異なるやり方をしていたので、ここでテンプレとなる型をまとめようと思います。

ここでは、Kerasでのニューラルネットワークモデルを使ったクロスバリデーションとしています。

LightGBMでのクロスバリデーションは以下でまとめています。

kaggleでよく使う交差検証テンプレ(LightGBM向け)
はじめにkaggleなどの機械学習コンペでLightGBMを使ってクロスバリデーションをする際のテンプレとなる型をまとめました。Kerasでのテンプレは以下でまとめています。内容については重複している部分もあるので、適宜読み飛ばしてくだ...

対象読者

  • kaggleでクロスバリデーションを使いたい人

クロスバリデーション(交差検証)とは

まずは、そもそもクロスバリデーションとは何かについて、簡単に説明します。

クロスバリデーション(交差検証)とは、学習用のデータを複数の分割パターンで学習データと検証データに分けてモデルの汎化性能(未知のデータに対する予測能力)を検証することです。

分割した学習データで学習し、検証データで予測し精度を検証します。複数の分割パターンで検証を行うことでより正確なモデルの精度を測ることができます。最終的なテストデータへの予測は、各モデルでの平均とすることが多いです。

分割したデータはfoldと呼ばれます。


scikit-learnのドキュメントより

kaggleでは提出用のテストデータは、PublicとPrivateに分かれており、最終的な順位はPrivateのテストデータで行われます。Publicのスコアが良くても、たまたまPublicのテストデータをうまく予測できていただけの可能性もあるため、Privateになった時に順位が大きく下がってしまうことがあります。そこで、クロスバリデーションのスコアでモデルの精度を検証することで、より正確にモデルの精度を検証することができるようになります。

よく使うクロスバリデーションとテンプレ

具体的なクロスバリデーションの手法とテンプレを紹介していきます。

前提

テンプレの前に、前提となる学習データの変数を定義しておきます。基本的に、前処理などが終わったら以下の形にするようにします。

"""
X : numpy.ndarray
  学習データの特徴量
y : numpy.ndarray
  学習データの目的変数
"""

K-fold(k分割交差検証)

最もよく使う基本的なクロスバリデーションです。

学習データをk個に分割し、k-1個分の学習データと1個分の検証データに分けてモデルの汎化性能を検証します。k回学習を繰り返し、分割したデータはそれぞれ1回ずつ検証データとして使います。

最終的にモデルの汎化性能を測る時は、各foldにおけるスコアを平均します。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのKFoldを利用
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はMAE
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import KFold
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

valid_scores = []
models = []
kf = KFold(n_splits=FOLD, shuffle=True, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(kf.split(X)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = mean_absolute_error(y_valid, y_valid_pred)
    print(f'fold {fold} MAE: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

Shuffle Split

データ全体からランダムにサンプリングして分割する交差検証です。

K-foldの代替手段として利用でき、ランダムにサンプリングしているため各foldでのデータを重複がありえます。そのため、検証データとして利用されてないデータも出てくる可能性があります。

全体を重複なく分割しているわけではないので、分割数と学習データと検証データの比率を自由に調整することができます。

K-foldでshuffle=Trueのオプションがありますが、これはデータ全体をシャッフルしてから重複なくデータを分割しているので、Shuffle Splitとは異なる結果(分割)になります。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのShuffleSplitを利用
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はMAE
  • 検証データは25%
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import ShuffleSplit
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

valid_scores = []
models = []
ss = ShuffleSplit(n_splits=FOLD, test_size=0.25, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(ss.split(X)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0

    y_valid_pred = model.predict(X_valid)
    score = mean_absolute_error(y_valid, y_valid_pred)
    print(f'fold {fold} MAE: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

Stratified k-fold(層化k分割交差検証)

各foldに含まれるクラスの割合を等しくするk分割交差検証です。

分類問題で使用され、テストデータと学習データのクラスの割合が等しいと仮定される時に使用される手法です。多クラス分類問題で、ランダムに分割すると各foldのクラスの割合が偏ってしまうような場合(例えば頻度の少ないクラスがある場合)に重要となってきます。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのStratifiedKFoldを利用
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はlog loss
from sklearn.metrics import log_loss
from sklearn.model_selection import StratifiedKFold
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

valid_scores = []
models = []
kf = StratifiedKFold(n_splits=FOLD, shuffle=True, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(kf.split(X, y)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = log_loss(y_valid, y_valid_pred)
    print(f'fold {fold} log loss: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

Stratified Shuffle Split

Stratified k-foldのShuffle Split版になります。

クラスの割合を保ったままShuffle Splitをおこないます。Shuffle Split同様、検証データにならないデータがある可能性があります。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのStratifiedShuffleSplitを利用
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はlog loss
  • 検証データは25%
from sklearn.metrics import log_loss
from sklearn.model_selection import StratifiedShuffleSplit
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

valid_scores = []
models = []
sss = StratifiedShuffleSplit(n_splits=FOLD, test_size=0.25, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(sss.split(X, y)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = log_loss(y_valid, y_valid_pred)
    print(f'fold {fold} log loss: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

Group k-fold(グループk分割交差検証)

同じグループ(顧客や被験者など特定の人物を表すものなど)が同じfoldになるようにデータを分割するk分割交差検証です。

テストデータで学習データのグループが現れないような場合に利用します。つまり、未知のグループを予測するような問題であるため、同じグループが異なるfoldに存在してしまうと、検証データにそのグループが含まれている時に予測しやすくなってしまい、適切な検証ができなくなってしまいます。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのGroupKFoldを利用
    • GroupKFoldはシャッフルと乱数の指定ができない
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はMAE
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GroupKFold
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

group = train['id']
valid_scores = []
models = []
kf = GroupKFold(n_splits=FOLD)

for fold, (train_indices, valid_indices) in enumerate(kf.split(X, y, group)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = mean_absolute_error(y_valid, y_valid_pred)
    print(f'fold {fold} MAE: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

Group Shuffle Split

Group k-foldのShuffle Split版になります。

検証データで学習データのグループが現れないようにShuffle Splitをおこないます。Shuffle Split同様、検証データにならないデータがある可能性があります。


scikit-learnのドキュメントより

テンプレ

  • scikit-learnのGroupShuffleSplitを利用
  • ニューラルネットワークモデルはbuild_modelという関数を用意する
  • 学習時のコールバックはReduceLROnPlateauModelCheckpointEarlyStopping
  • 評価関数はMAE
  • 検証データは25%
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GroupShuffleSplit
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
import numpy as np

FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

group = train['id']
valid_scores = []
models = []
gss = GroupShuffleSplit(n_splits=FOLD, test_size=0.25, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(gss.split(X, y, group)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = mean_absolute_error(y_valid, y_valid_pred)
    print(f'fold {fold} MAE: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

実践してみる

実際にscikit-learnのデータセットであるbostonデータを使ってクロスバリデーションを実施してみます。

from sklearn.datasets import load_boston
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import KFold, train_test_split
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, EarlyStopping
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential

import numpy as np

# データの読み込み
boston = load_boston()
X, X_test, y, y_test = train_test_split(
    boston['data'], boston['target'], test_size=0.3, random_state=0)

# モデルの構築
def build_model(n_features):
    model = Sequential()
    model.add(Dense(64, activation='relu', input_shape=(n_features,)))
    model.add(Dense(64, activation='relu'))
    model.add(Dense(1))
    model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])
    return model

# k-fold cross validation
FOLD = 5
EPOCH = 10
BATCH_SIZE = 32

valid_scores = []
models = []
kf = KFold(n_splits=FOLD, shuffle=True, random_state=42)

for fold, (train_indices, valid_indices) in enumerate(kf.split(X)):
    X_train, X_valid = X[train_indices], X[valid_indices]
    y_train, y_valid = y[train_indices], y[valid_indices]

    model = build_model(X_train.shape[1])
    rlr = ReduceLROnPlateau(monitor='val_loss',
                            factor=0.1,
                            patience=3,
                            verbose=0,
                            min_delta=1e-4,
                            mode='max')
    ckp = ModelCheckpoint(f'model_{fold}.hdf5',
                          monitor='val_loss',
                          verbose=0,
                          save_best_only=True,
                          save_weights_only=True,
                          mode='max')
    es = EarlyStopping(monitor='val_loss',
                       min_delta=1e-4,
                       patience=7,
                       mode='max',
                       baseline=None,
                       restore_best_weights=True,
                       verbose=0)

    model.fit(X_train, y_train,
              validation_data=(X_valid, y_valid),
              epochs=EPOCH,
              batch_size=BATCH_SIZE,
              callbacks=[rlr, ckp, es],
              verbose=0)

    y_valid_pred = model.predict(X_valid)
    score = mean_absolute_error(y_valid, y_valid_pred)
    print(f'fold {fold} MAE: {score}')
    valid_scores.append(score)

    models.append(model)

cv_score = np.mean(valid_scores)
print(f'CV score: {cv_score}')

# テストデータの予測
preds = []
for model in models:
    pred = model.predict(X_test)
    preds.append(pred)

y_pred = np.mean(preds, axis=0)
print(f'Test Score MAE: {mean_absolute_error(y_pred, y_test)}')

出力結果は以下のようになります。

fold 0 MAE: 4.701173231635296
fold 1 MAE: 10.668852969962106
fold 2 MAE: 11.41734183405487
fold 3 MAE: 8.52358005953507
fold 4 MAE: 5.542828990391323
CV score: 8.170755417115732
Test Score MAE: 5.843269380142814

まとめ

今回は、以下のクロスバリデーションのテンプレを用意しました。

  • K Fold
  • Shuffle Split
  • Stratified K Fold
  • Stratified Shuffle Split
  • Group K Fold
  • Group Shuffle Split

ただし、なんでもこの6つでいいというわけではないので、問題によって適切なクロスバリデーションの手法を選択するのが重要になります。。

参考

タイトルとURLをコピーしました