Python, scikit-learn

【scikit-learn】k-分割交差検証の実装


機械学習モデルの性能を評価する手法の1つにk-分割交差検証があります。

本記事では、Pythonの機械学習ライブラリであるscikit-learnを活用してk-分割交差検証を行う処理の実装方法について解説します。

そもそも「k-分割交差検証」とは?

まずはk-分割交差検証とは何かについて簡単に説明します。

機械学習モデルの性能評価手法

機械学習モデルのテストでは、任意のデータに対して予測を行い、その予測がどの程度正しいかを調べます。

テストを行う際、モデルの学習に使用したデータは使用することができません。

何故ならモデルは学習データに対して正しい予測ができるように学習を進めるため、学習の完了したモデルに学習データを入力すればほぼ必ず正しい予測が得られるからです。

既に答えを知っている問題でテストをしても適切な性能評価は行えません。

そのため、機械学習モデルを構築する際は、元のデータセットを学習用とテスト用に分割して使用するのが一般的です。

k-分割交差検証では、このデータセットの分割を複数のパターンで行い、それぞれで実験(学習とテスト)を行います。

複数のパターンで学習とテストを行うことでより正確な性能評価が可能です。

「k-分割交差検証」の実装例を紹介

それでは、k-分割交差検証の実装例をいくつか紹介しましょう。

なお、紹介するサンプルプログラムの言語にはPythonを使用しており、機械学習ライブラリのscikit-learnを活用してモデルを構築しています。

実装例01. cross_val_scoreメソッドを利用する

最も簡単な方法はscikit-learnのcross_val_scoreメソッドを利用する方法です。

cross_val_scoreメソッドは、モデル(学習前)とデータセット、検証を行う回数を指定すると自動でk-分割交差検証を実施し、各検証のテストのスコアを返してくれます。

cross_val_scoreメソッドに設定できる引数(一部省略)と戻り値は以下の通りです。

  • 【引数】estimator(object):機械学習モデル
  • 【引数】X(array):入力データ
  • 【引数】y(array):正解データ
  • 【引数】cv(int):k-分割交差検証の実施回数
  • 【戻り値】k-分割交差検証の各テストスコア

この方法でのk-分割交差検証の実装例を以下に示します。

from sklearn.model_selection import cross_val_score
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    mlp = MLPClassifier()
    cvs = cross_val_score(mlp, x, y, cv=iter)
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')
    for i,score in enumerate(cvs):
        print('  k={0}: {1}'.format(i, score))

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 1.0
  k=1: 1.0
  k=2: 0.9333333333333333
  k=3: 0.9333333333333333
  k=4: 1.0

実装例02. KFoldクラスを利用する

scikit-learnのKFoldクラスを利用する方法もあります。

KFoldクラスはk-分割交差検証を行うための機能を提供するクラスです。分割した部分データセットが最低1度はテスト用に使われるように検証を行います。

KFoldクラスに設定できる引数(一部省略)は以下の通りです。

  • 【引数】n_split(int):データセットの分割する数(検証の実施回数)
  • 【引数】shuffle(bool):データシャッフルするか否か
  • 【引数】random_state(int):分割に使用する乱数のシード

この方法でのk-分割交差検証の実装例を以下に示します。

from sklearn.model_selection import KFold
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')

    i = 0
    kf = KFold(n_splits=iter, shuffle=True, random_state=0)
    for train_i, test_i in kf.split(x):
        train_x, test_x = x[train_i], x[test_i]
        train_y, test_y = y[train_i], y[test_i]
        mlp = MLPClassifier().fit(train_x, train_y)
        print('  k={0}: {1}'.format(i, mlp.score(test_x, test_y)))
        i += 1

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 1.0
  k=1: 1.0
  k=2: 0.7333333333333333
  k=3: 1.0
  k=4: 0.5

実装例03. StratifiedKFoldクラスを利用する

scikit-learnのStratifiedKFoldクラスを利用する方法もあります。

StratifiedKFoldクラスもk-分割交差検証を行うための機能を提供するクラスです。クラス別のデータ件数の比率を分割後の部分データセットにも反映して検証を行います。

StratifiedKFoldクラスに設定できる引数(一部省略)は以下の通りです。

  • 【引数】n_split(int):データセットの分割する数(検証の実施回数)
  • 【引数】shuffle(bool):データシャッフルするか否か
  • 【引数】random_state(int):分割に使用する乱数のシード

この方法でのk-分割交差検証の実装例を以下に示します。

from sklearn.model_selection import StratifiedKFold
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')

    i = 0
    kf = StratifiedKFold(n_splits=iter, shuffle=True, random_state=0)
    for train_i, test_i in kf.split(x):
        train_x, test_x = x[train_i], x[test_i]
        train_y, test_y = y[train_i], y[test_i]
        mlp = MLPClassifier().fit(train_x, train_y)
        print('  k={0}: {1}'.format(i, mlp.score(test_x, test_y)))
        i += 1

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 1.0
  k=1: 0.9
  k=2: 1.0
  k=3: 1.0
  k=4: 0.9333333333333333

実装例04. ShuffleSplitクラスを利用する

scikit-learnのShuffleSplitクラスを利用する方法もあります。

ShuffleSplitクラスもk-分割交差検証を行うための機能を提供するクラスです。データセットのシャッフルは必ず実施され、テストに用いるデータの比率を個別に指定できます。

ShuffleSplitクラスに設定できる引数(一部省略)は以下の通りです。

  • 【引数】n_split(int):データセットの分割する数(検証の実施回数)
  • 【引数】test_size(float):テスト用データの比率(0~1)
  • 【引数】random_state(int):分割に使用する乱数のシード

この方法でのk-分割交差検証の実装例を以下に示します。

from sklearn.model_selection import ShuffleSplit
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')

    i = 0
    ss = ShuffleSplit(n_splits=iter, test_size=test_size, random_state=0)
    for train_i, test_i in ss.split(x):
        train_x, test_x = x[train_i], x[test_i]
        train_y, test_y = y[train_i], y[test_i]
        mlp = MLPClassifier().fit(train_x, train_y)
        print('  k={0}: {1}'.format(i, mlp.score(test_x, test_y)))
        i += 1

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 1.0
  k=1: 0.9666666666666667
  k=2: 0.9333333333333333
  k=3: 1.0
  k=4: 1.0

実装例05. train_test_splitメソッドを利用する

scikit-learnのtrain_test_splitメソッドを利用する方法もあります。

train_test_splitメソッドは与えられたリストを複数個に分割するメソッドであり、このメソッドを用いてデータセット分割、検証の流れを繰り返せばk-分割交差検証を実装できます。

train_test_splitメソッドに設定できる引数と戻り値は以下の通りです。

  • 【引数】分割対象のリスト(複数指定可)
  • 【引数】test_size(int or float):テストデータの件数or比率
  • 【引数】train_size(int or float):学習データの件数or比率
  • 【引数】random_state(int or None):分割に使用する乱数のシード
  • 【引数】shuffle(bool):データをシャッフルするか否か
  • 【戻り値】分割後のリスト(複数個)

この方法でのk-分割交差検証の実装例を以下に示します。

from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')
    for i in range(iter):
        train_x, test_x, train_y, test_y = train_test_split(x, y, test_size=test_size, random_state=i)
        mlp = MLPClassifier().fit(train_x, train_y)
        print('  k={0}: {1}'.format(i, mlp.score(test_x, test_y)))

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 1.0
  k=1: 1.0
  k=2: 0.9333333333333333
  k=3: 0.9333333333333333
  k=4: 1.0

実装例06. スライスによるリスト分割を利用する

最後はスライスによるリスト分割を利用する方法です。

スライス操作によってリストを任意のインデックス以前と以後に分割することでk-分割交差検証を実装します。

from sklearn.neural_network import MLPClassifier
from sklearn.datasets import load_iris
import random

if __name__ == '__main__':

    iter = 5 # 実施する交差検証の回数を指定
    test_size = float(1) / iter # テストに用いるデータの比率を指定

    # 実験用データセットの読み込み
    iris = load_iris()
    x = iris.data
    y = iris.target

    # k-分割交差検証の実施
    print('Cross Validation ( Iter = {0} )'.format(iter))
    print('-> Train data size : {:.0f}'.format(len(x) - test_size * len(x)))
    print('-> Test data size  : {:.0f}'.format(len(x) * test_size))
    print('------------------------')
    index = int(test_size * len(x))
    for i in range(iter):
        pair = list(zip(x,y))
        random.shuffle(pair)
        tuple_x, tuple_y = zip(*pair)
        x = list(tuple_x)
        y = list(tuple_y)
        train_x, test_x = x[index:], x[:index]
        train_y, test_y = y[index:], y[:index]
        mlp = MLPClassifier()
        mlp.fit(train_x, train_y)
        print('  k={0}: {1}'.format(i, mlp.score(test_x, test_y)))

このプログラムを実行すると以下の出力結果が得られます。

Cross Validation ( Iter = 5 )
-> Train data size : 120
-> Test data size  : 30
------------------------
  k=0: 0.9666666666666667
  k=1: 0.9333333333333333
  k=2: 0.9333333333333333
  k=3: 0.9333333333333333
  k=4: 0.9666666666666667

なお、スライス操作だけでは元のデータセットを格納したリストの中身が[0,0…1,1…2,2]のように特定のグループごとの塊になっている場合は上手く分割できません。

分割後のリストの中身に偏りが出てしまうからです。

これは元のデータセットの中身をシャッフルする処理を同時に実装すれば解決できますが、少し面倒なのでこのやり方はあまりおすすめしません。