[Python] k近傍法 (3) 交差検証

前回からの続きです。

前回からの続きです。 あやめの分類問題は素での正答率が高すぎたので、今回は同じくscikit-learn に含まれるワインのデータセットを使い、 k近傍法で予測を行います。 その後、説明変数の標準化を行ってから k近傍法で予測を行い、予測がどのよ...

今回は、あやめの分類問題を使い、交差検証 cross validattion を行います。

ホールドアウト検証

これまでは、データを訓練データとテストデータを分けるだけで、訓練データをモデルの学習に使い、テストデータを使いモデルの評価を行いました。

これは、「 初期標本群から事例を無作為に選択してテスト事例を形成し、残る事例を訓練事例とする」ホールドアウト検証と呼ばれる検証方法です。

ホールドアウト検証でモデルの予測は、用いた訓練データにのみ依存します。

交差検証

交差検証交差確認[1](こうさけんしょう、: Cross-validation)とは、統計学において標本データを分割し、その一部をまず解析して、残る部分でその解析のテストを行い、解析自身の妥当性の検証・確認に当てる手法を指す[2][3][4]。データの解析(および導出された推定・統計的予測)がどれだけ本当に母集団に対処できるかを良い近似で検証・確認するための手法である。

出典: フリー百科事典『ウィキペディア(Wikipedia)』

例えば、K-分割交差検証は、データをK個に分割することで、Kをテストデータ、K以外を訓練データとして用いる学習がK回行うことができるようになるので、ホールドアウト検証と比較した場合、モデルの性能をより一般的に検証できるであろうという考え方です。

sckit -learn では、ユーザーガイドの以下の章で、交差検証がまとめられています。

3.1. Cross-validation: evaluating estimator performance

IIDであるようなデータに対しては、一般的な規則として、5-分割交差検証か 10-分割交差検証を行えば良いそうです。

時系列に沿ったデータや、構造的にグループに分けることのできるデータなど、適当に分割すると問題が起こるデータである場合にそれぞれどのように分割を行うかも、上にまとめられています。

あやめの分類問題で交差検証

あやめのデータはIIDなので5-分割交差検証を行います。

ただし、今回のような分類問題では、テストデータと訓練データの分割の際に stratify という引数を指定したように、検証データと訓練データで目的変数の割合が異なると良くないので、以下の stratified された KFold を用いる必要があります。

sklearn.model_selection.StratifiedKFold

まずは、交差検証を行わないで、k近傍法であやめの分類を行います。

In [1]:  from sklearn.datasets import load_iris
   ...:    ...: import pandas as pd
   ...:    ...: iris = load_iris()
   ...:    ...: df = pd.DataFrame(iris.data, columns=iris.feature_names)

In [2]: from sklearn.model_selection import train_test_split
   ...: X_train, X_test, y_train, y_test = train_test_split(df[iris.feature_names],
   ...:                                                     iris.target,
   ...:                                                     test_size=0.2,
   ...:                                                     stratify=iris.target,
   ...:                                                     random_state=0)

In [3]: from sklearn.neighbors import KNeighborsClassifier
   ...: knn = KNeighborsClassifier()
   ...: knn.fit(X_train, y_train)
Out[3]: 
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
                     metric_params=None, n_jobs=None, n_neighbors=5, p=2,
                     weights='uniform')

In [4]: knn.score(X_test, y_test)
Out[4]: 1.0

次に、以下の関数を用いることで、交差検証を行い、交差検証のスコアを出してみます。

sklearn.model_selection.cross_val_score

この関数は、モデルのクラスが sklearn.base.ClassifierMixin に属する場合は、自動的に StratifiedKFold を適用してくれるそうです。

In [5]: import numpy as np
   ...: from sklearn.model_selection import cross_val_score
   ...: scores = cross_val_score(knn, X_train, y_train, cv=5)
   ...: scores
Out[5]: array([0.95833333, 0.95833333, 0.91666667, 1.        , 0.91666667])

In [6]: np.mean(scores)
Out[6]: 0.95

In [7]: np.std(scores)
Out[7]: 0.0311804782231162

それぞれスコアがある程度ばらつき、現状のデータをk近傍法で分析した場合、モデルは訓練データにかなり依存することがわかります。

sickit-learn のユーザーガイドに従い、スコアの95%信頼区間を出します。

In [8]: print(f'Accuracy: {np.mean(scores):.2f} (+/- {np.std(scores)*2:.2f})')
Accuracy: 0.95 (+/- 0.06)

今度は、テストデータも使い、モデルの検証を行います。

In [56]: from sklearn.model_selection import StratifiedKFold
    ...: skf = StratifiedKFold(n_splits=5)
    ...: for kfold_id, (train_index, valid_index) in enumerate(skf.split(X_train, y_train)):
    ...:     X_train_k =  X_train.iloc[train_index, :]
    ...:     X_valid = X_train.iloc[valid_index, :]
    ...:     y_train_k, y_valid = y_train[train_index], y_train[valid_index]
    ...: 
    ...:     knn.fit(X_train_k, y_train_k)
    ...:     valid_score = knn.score(X_valid, y_valid)
    ...:     test_score = knn.score(X_test, y_test)
    ...: 
    ...:     print(f'{kfold_id+1}番目のモデルの正解率は、検証では{valid_score:.2f}、テストでは{test_score:.2f}です。')
    ...: 
1番目のモデルの正解率は、検証では0.96、テストでは1.00です。
2番目のモデルの正解率は、検証では0.96、テストでは1.00です。
3番目のモデルの正解率は、検証では0.92、テストでは1.00です。
4番目のモデルの正解率は、検証では1.00、テストでは1.00です。
5番目のモデルの正解率は、検証では0.92、テストでは1.00です。

テストの検証が全て成功しています。

訓練データとテストデータの分割を変えて行ってみます。

In [57]: from sklearn.model_selection import train_test_split
    ...: X_train, X_test, y_train, y_test = train_test_split(df[iris.feature_names],
    ...:                                                     iris.target,
    ...:                                                     test_size=0.2,
    ...:                                                     stratify=iris.target,
    ...:                                                     random_state=5)

In [58]: from sklearn.model_selection import StratifiedKFold
    ...: skf = StratifiedKFold(n_splits=5)
    ...: for kfold_id, (train_index, valid_index) in enumerate(skf.split(X_train, y_train)):
    ...:     X_train_k =  X_train.iloc[train_index, :]
    ...:     X_valid = X_train.iloc[valid_index, :]
    ...:     y_train_k, y_valid = y_train[train_index], y_train[valid_index]
    ...: 
    ...:     knn.fit(X_train_k, y_train_k)
    ...:     valid_score = knn.score(X_valid, y_valid)
    ...:     test_score = knn.score(X_test, y_test)
    ...: 
    ...:     print(f'{kfold_id+1}番目のモデルの正解率は、検証では{valid_score:.2f}、テストでは{test_score:.2f}です。')
    ...: 
1番目のモデルの正解率は、検証では0.96、テストでは0.97です。
2番目のモデルの正解率は、検証では1.00、テストでは0.97です。
3番目のモデルの正解率は、検証では0.92、テストでは1.00です。
4番目のモデルの正解率は、検証では0.96、テストでは0.97です。
5番目のモデルの正解率は、検証では0.96、テストでは0.97です。

どのデータで訓練をするか、どのデータで検証を行うかによって異なるモデル、異なる結果になることから、交差検証の必要性が理解できます。