機械学習 – ホールドアウト、クロスバリデーションについて

機械学習 – ホールドアウト、クロスバリデーションについて

概要

機械学習におけるモデルの評価方法であるホールドアウト、交差検証 (クロスバリデーション) について解説します。

Advertisement

学習とテストで別のデータセットを使用する理由

学習に使用したデータと同じデータでテストを行ってモデルの精度を評価した場合、そのモデルが学習データを認識することに特化したものとなる過剰適合が発生していることを見抜けず、モデルの汎化性能を正しく評価できません。そのため、通常はデータセットはモデルのパラメータ調整に使用する学習用と学習には一切使用しないテスト用に分割することが一般的です。

ホールドアウトと交差検証 (クロスバリデーション)

モデルに予め設定しておくパラメータ (ハイパーパラメータ) がある場合、学習を試行錯誤し、ハイパーパラメータを調整する必要があります。その評価にテストデータを使用してしまうと、ハイパーパラメータがテストデータに特化した過学習状態となる可能性を見抜けなくなります。そのため、データセットを学習用、テスト用の他にハイパーパラメータを調整する検証用の3つに分割します。しかし、この方法でも検証用のデータに特化したハイパーパラメータになってしまう可能性や3つに分割すると学習に使用するデータが減ってしまう欠点があるため、次に紹介する交差検証 (クロスバリデーション) により、ハイパーパラメータを決める方法もあります。

  • ハイパーパラメータの調整あり
    • ホールドアウト: データセットを学習用、検証用、テスト用の3つに分割し、検証用を使用してハイパーパラメータを調整します。テスト用は最終的なモデルの評価に使用します。
    • クロスバリデーション: データセットを学習及び検証用、テスト用の2つに分割し、クロスバリデーションにより、ハイパーパラメータを調整します。テスト用は最終的なモデルの評価に使用します。
  • ハイパーパラメータの調整なし
    • データセットを学習用、テスト用の2つに分割します。テスト用は最終的なモデルの評価に使用します。

データの分割

ホールドアウト、クロスバリデーションどちらを行うにしても、最低でもデータセットを学習用とテスト用に分割することは必須です。

ホールドアウト

sklearn.model_selection.train_test_split

scikit-learn の train_test_split() を使用すると、データセットを学習用とテスト用に分割できます。

sklearn.model_selection.train_test_split(
    *arrays,
    test_size=None,
    train_size=None,
    random_state=None,
    shuffle=True,
    stratify=None
)
  • arrays: 分割対象の配列 (複数個指定可)。
    • 配列の型は リスト、numpy 配列、scipy sparse 行列、pandas dataframes に対応
  • test_size: テストに利用する割合。float, int, None の指定が可能。
    • デフォルト: train_size を指定した場合は None、そうでない場合は 0.25。
    • float: 分割の割合を [0, 1] の範囲で指定する。
    • int: テストに使用するデータ数を指定する。
    • None: train_size の指定に従い、決める。
  • train_size: 学習に利用する割合。float, int, None の指定が可能。
    • デフォルト: test_size を指定した場合は None。
    • float: 分割の割合を [0, 1] の範囲で指定する。
    • int: テストに使用するデータ数を指定する。
    • None: test_size の指定に従い、決める。
  • random_state: 分割に使用する乱数に関する設定。
    • デフォルト: None
    • int: 乱数のシードに利用
    • None: np.random.RandomState オブジェクトを利用する。
  • shuffle: 分割前にシャッフルするかどうかを指定する。
    • デフォルト: True
  • stratify: 各クラスごとに同じ割合で分割したい場合は、クラス一覧を指定する。
    • デフォルト: None

複数の配列を一度に分割できます。データとラベルがある場合は両方指定します。

In [1]:
import numpy as np
from sklearn.model_selection import train_test_split

# 分割対象の配列を作成する。
data = np.arange(20)
labels = np.arange(20)

# array1, array2 をそれぞれ学習データ75%、テストデータ25%の割合で分割する。
data_train, data_test, labels_train, labels_test = train_test_split(
    data, labels, train_size=0.75, random_state=0
)

print("data train", data_train)
print("data test", data_test)
print("labels train", labels_train)
print("labels test", labels_test)
data train [17  6 13  4  2  5 14  9  7 16 11  3  0 15 12]
data test [18  1 19  8 10]
labels train [17  6 13  4  2  5 14  9  7 16 11  3  0 15 12]
labels test [18  1 19  8 10]

実際のデータセットを分割した例です。

In [2]:
from sklearn import datasets, metrics, svm
from sklearn.model_selection import train_test_split

X, y = datasets.load_iris(return_X_y=True)
print(X.shape, y.shape)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.15, random_state=0
)

print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
(150, 4) (150,)
(127, 4) (127,)
(23, 4) (23,)
Advertisement

クロスバリデーション

クロスバリデーション (cross validation) は、まず学習用データセットをいくつかの Fold に分割します。

Fold の分割

次に、1つを除いた残りの Fold で学習を行い、残り1つの Fold で評価するというのを分割数だけ繰り返します。各評価の平均をとることでクロスバリデーションによるモデルの評価結果とします。ハイパーパラメータの調整を行う場合は、パラメータを変更し、クロスバリデーションを行うサイクルを繰り返し、最良の結果が出たハイパーパラメータを採用します。

クロスバリデーション

ハイパーパラメータが決定したら、そのパラメータで学習用をすべて使い学習を行い、テスト用で最終的なモデルの評価結果にします。

sklearn.model_selection.cross_val_score

scikit-learn の cross_val_score() を使用すると、クロスバリデーションが行えます。返り値は各試行のスコアです。

sklearn.model_selection.cross_val_score(estimator, X, y=None, *, groups=None, scoring=None, cv=None, n_jobs=None, verbose=0, fit_params=None, pre_dispatch='2*n_jobs', error_score=nan)
In [3]:
from sklearn import datasets, metrics, svm
from sklearn.model_selection import cross_val_score, train_test_split

X, y = datasets.load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.15, random_state=0
)

clf = svm.SVC(kernel="linear", C=1, random_state=0)
scores = cross_val_score(clf, X_train, y_train, cv=5)
print(scores)
[0.96153846 0.92307692 1.         1.         0.88      ]

sklearn.model_selection.cross_validate

scikit-learn の cross_validate() は、cross_val_score() とほとんど同じですが、以下の違いがあります。

  1. 複数の評価基準を指定できます。
  2. 返り値で学習データに対する評価値、学習や評価にかかった時間など追加の情報を取得できます。
sklearn.model_selection.cross_validate(
    estimator,
    X,
    y=None,
    *,
    groups=None,
    scoring=None,
    cv=None,
    n_jobs=None,
    verbose=0,
    fit_params=None,
    pre_dispatch="2*n_jobs",
    return_train_score=False,
    return_estimator=False,
    error_score=nan,
)
In [4]:
from sklearn import datasets, metrics, svm
from sklearn.model_selection import cross_validate, train_test_split

X, y = datasets.load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.15, random_state=0
)

clf = svm.SVC(kernel="linear", C=1, random_state=0)
scores = cross_validate(clf, X_train, y_train, cv=5)
print(scores)
{'fit_time': array([0.00070715, 0.00064349, 0.00065446, 0.00063968, 0.00063086]), 'score_time': array([0.00026226, 0.00024509, 0.00024509, 0.00024366, 0.00024295]), 'test_score': array([0.96153846, 0.92307692, 1.        , 1.        , 0.88      ])}

sklearn.model_selection.cross_val_predict

scikit-learn の cross_val_predict() は、1つの Fold を除いたデータで学習し、1つの Fold を予測した結果を返します。

sklearn.model_selection.cross_val_predict(
    estimator,
    X,
    y=None,
    *,
    groups=None,
    cv=None,
    n_jobs=None,
    verbose=0,
    fit_params=None,
    pre_dispatch="2*n_jobs",
    method="predict",
)
  • estimator: モデル
  • X: データ
  • y: ラベル
  • cv: 分割方法
In [5]:
from sklearn import datasets, metrics, svm
from sklearn.model_selection import cross_val_predict, train_test_split

X, y = datasets.load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.15, random_state=0
)

clf = svm.SVC(kernel="linear", C=1, random_state=0)
scores = cross_val_predict(clf, X_train, y_train, cv=5)
print(scores)
[0 2 0 0 1 1 0 2 1 0 2 2 1 0 2 1 1 2 0 2 0 0 1 2 2 2 2 1 2 1 1 2 1 1 2 1 2
 1 0 2 1 1 1 1 2 0 0 2 1 0 0 1 0 2 1 0 1 2 1 0 2 2 2 2 0 0 2 2 0 2 0 2 2 0
 0 2 0 0 0 1 2 2 0 0 0 1 1 0 0 1 0 2 1 2 1 0 2 0 2 0 0 2 0 2 1 1 1 2 2 2 2
 0 1 2 2 0 1 1 2 1 0 0 0 2 1 2 0]