📋

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

2021/02/06に公開

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

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

テンプレの追記などの修正は下記でしています。

https://amateur-engineer-blog.com/kaggle-cv-template/

また、LightGBMでのクロスバリデーションのテンプレは下記でまとめています。

https://amateur-engineer-blog.com/kaggle-cv-template-lightgbm/

対象読者

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

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

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

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

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

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


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

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

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

ここからkaggleでよく使われる具体的なクロスバリデーションの手法とテンプレを紹介していきます。

前提

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

"""
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

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}')

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

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}')

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

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}')

実践してみる

実際に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

まとめ

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

  • K Fold
  • Stratified K Fold
  • Group K Fold

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

参考

Discussion