解析環境セットアップ

ライブラリーの準備

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

データセットの準備

このノートで、モデルの評価を中心に行うために、サンプル数の多い乳がんのデータを使用する。このデータは以下のように 569 サンプルからなる。目的変数は良性と悪性のいずれかとなっている。

In [2]:
import sklearn.datasets
cancer = sklearn.datasets.load_breast_cancer()
X = cancer.data
y = cancer.target

print(X.shape)
print(X[1:10, :])
print(y[1:10])
(569, 30)
[[2.057e+01 1.777e+01 1.329e+02 1.326e+03 8.474e-02 7.864e-02 8.690e-02
  7.017e-02 1.812e-01 5.667e-02 5.435e-01 7.339e-01 3.398e+00 7.408e+01
  5.225e-03 1.308e-02 1.860e-02 1.340e-02 1.389e-02 3.532e-03 2.499e+01
  2.341e+01 1.588e+02 1.956e+03 1.238e-01 1.866e-01 2.416e-01 1.860e-01
  2.750e-01 8.902e-02]
 [1.969e+01 2.125e+01 1.300e+02 1.203e+03 1.096e-01 1.599e-01 1.974e-01
  1.279e-01 2.069e-01 5.999e-02 7.456e-01 7.869e-01 4.585e+00 9.403e+01
  6.150e-03 4.006e-02 3.832e-02 2.058e-02 2.250e-02 4.571e-03 2.357e+01
  2.553e+01 1.525e+02 1.709e+03 1.444e-01 4.245e-01 4.504e-01 2.430e-01
  3.613e-01 8.758e-02]
 [1.142e+01 2.038e+01 7.758e+01 3.861e+02 1.425e-01 2.839e-01 2.414e-01
  1.052e-01 2.597e-01 9.744e-02 4.956e-01 1.156e+00 3.445e+00 2.723e+01
  9.110e-03 7.458e-02 5.661e-02 1.867e-02 5.963e-02 9.208e-03 1.491e+01
  2.650e+01 9.887e+01 5.677e+02 2.098e-01 8.663e-01 6.869e-01 2.575e-01
  6.638e-01 1.730e-01]
 [2.029e+01 1.434e+01 1.351e+02 1.297e+03 1.003e-01 1.328e-01 1.980e-01
  1.043e-01 1.809e-01 5.883e-02 7.572e-01 7.813e-01 5.438e+00 9.444e+01
  1.149e-02 2.461e-02 5.688e-02 1.885e-02 1.756e-02 5.115e-03 2.254e+01
  1.667e+01 1.522e+02 1.575e+03 1.374e-01 2.050e-01 4.000e-01 1.625e-01
  2.364e-01 7.678e-02]
 [1.245e+01 1.570e+01 8.257e+01 4.771e+02 1.278e-01 1.700e-01 1.578e-01
  8.089e-02 2.087e-01 7.613e-02 3.345e-01 8.902e-01 2.217e+00 2.719e+01
  7.510e-03 3.345e-02 3.672e-02 1.137e-02 2.165e-02 5.082e-03 1.547e+01
  2.375e+01 1.034e+02 7.416e+02 1.791e-01 5.249e-01 5.355e-01 1.741e-01
  3.985e-01 1.244e-01]
 [1.825e+01 1.998e+01 1.196e+02 1.040e+03 9.463e-02 1.090e-01 1.127e-01
  7.400e-02 1.794e-01 5.742e-02 4.467e-01 7.732e-01 3.180e+00 5.391e+01
  4.314e-03 1.382e-02 2.254e-02 1.039e-02 1.369e-02 2.179e-03 2.288e+01
  2.766e+01 1.532e+02 1.606e+03 1.442e-01 2.576e-01 3.784e-01 1.932e-01
  3.063e-01 8.368e-02]
 [1.371e+01 2.083e+01 9.020e+01 5.779e+02 1.189e-01 1.645e-01 9.366e-02
  5.985e-02 2.196e-01 7.451e-02 5.835e-01 1.377e+00 3.856e+00 5.096e+01
  8.805e-03 3.029e-02 2.488e-02 1.448e-02 1.486e-02 5.412e-03 1.706e+01
  2.814e+01 1.106e+02 8.970e+02 1.654e-01 3.682e-01 2.678e-01 1.556e-01
  3.196e-01 1.151e-01]
 [1.300e+01 2.182e+01 8.750e+01 5.198e+02 1.273e-01 1.932e-01 1.859e-01
  9.353e-02 2.350e-01 7.389e-02 3.063e-01 1.002e+00 2.406e+00 2.432e+01
  5.731e-03 3.502e-02 3.553e-02 1.226e-02 2.143e-02 3.749e-03 1.549e+01
  3.073e+01 1.062e+02 7.393e+02 1.703e-01 5.401e-01 5.390e-01 2.060e-01
  4.378e-01 1.072e-01]
 [1.246e+01 2.404e+01 8.397e+01 4.759e+02 1.186e-01 2.396e-01 2.273e-01
  8.543e-02 2.030e-01 8.243e-02 2.976e-01 1.599e+00 2.039e+00 2.394e+01
  7.149e-03 7.217e-02 7.743e-02 1.432e-02 1.789e-02 1.008e-02 1.509e+01
  4.068e+01 9.765e+01 7.114e+02 1.853e-01 1.058e+00 1.105e+00 2.210e-01
  4.366e-01 2.075e-01]]
[0 0 0 0 0 0 0 0 0]

前処理

scikit-learn が提供している前処理用の関数は sklearn.preprocessing モジュールに実装されています。このノートではほとんど使わないが、今後、自分のデータを使ってモデルを作る場合に役だったりしますので、ここで使い方を軽くだけ紹介する。

カテゴリデータをゼロから始まる整数値に変更したい場合は OrdinalEncoder 変換器を使用する。例えば、1 列目が米の品種名、2 列目が県名となっているデータを OrdinalEncoder 変換器処理すると、両方とも整数化された結果が得られる。ただし、このまま整数化されたデータを特徴量として機械学習に代入すると、コシヒカリが 1.0、アキタコマチが 0.0 というなんらかの量と見なされて、学習されてしまう。そのため、OrdinalEncoder を使用して整数化するときは、注意すること。

In [3]:
import sklearn.preprocessing
enc = sklearn.preprocessing.OrdinalEncoder()

x = [['koshihikari',  'niigata'],
     ['akitakomachi', 'akita'],
     ['koshihikari',  'chiba'],
     ['koshihikari',  'toyama'],
     ['nanatsuboshi', 'hokkaido']]


print('Original data:')
print(x)

enc.fit(x)
x_enc = enc.transform(x)

print('Transformed data:')
print(x_enc)
Original data:
[['koshihikari', 'niigata'], ['akitakomachi', 'akita'], ['koshihikari', 'chiba'], ['koshihikari', 'toyama'], ['nanatsuboshi', 'hokkaido']]
Transformed data:
[[1. 3.]
 [0. 0.]
 [1. 1.]
 [1. 4.]
 [2. 2.]]

次のようにして、自分で整数化する際の順番を設定することもできる。

In [4]:
x1 = ['nanatsuboshi', 'akitakomachi', 'koshihikari']
x2 = ['hokkaido', 'akita', 'niigata', 'chiba', 'toyama']

# numpy 配列にするとエラーがでるようなので 2 次元リストを利用する
enc = sklearn.preprocessing.OrdinalEncoder(categories=[x1, x2])

print('Original data:')
print(x)

enc.fit(x)
x_enc = enc.transform(x)

print('Transformed data:')
print(x_enc)
Original data:
[['koshihikari', 'niigata'], ['akitakomachi', 'akita'], ['koshihikari', 'chiba'], ['koshihikari', 'toyama'], ['nanatsuboshi', 'hokkaido']]
Transformed data:
[[2. 2.]
 [1. 1.]
 [2. 3.]
 [2. 4.]
 [0. 0.]]

カテゴリデータの場合は一般的に one hot encodeing で数値化する。scikit-learn では OneHotEncoder を使用して変換を行う。この変換を行うと、出力結果は scikit-learn のスパース行列の形で保存されます。そのままで理解しにくいので、このスパース行列を普通の行列に直して表示させてみる。

In [5]:
import sklearn.preprocessing

enc = sklearn.preprocessing.OneHotEncoder()

x = [['koshihikari',  'niigata'],
     ['akitakomachi', 'akita'],
     ['koshihikari',  'chiba'],
     ['koshihikari',  'toyama'],
     ['nanatsuboshi', 'hokkaido']]

print('Original data:')
print(x)

enc.fit(x)
x_enc = enc.transform(x)
print('Transformed data (sparse matrix):')
print(x_enc)

x_enc = x_enc.toarray()
print('Transformed data:')
print(x_enc)
Original data:
[['koshihikari', 'niigata'], ['akitakomachi', 'akita'], ['koshihikari', 'chiba'], ['koshihikari', 'toyama'], ['nanatsuboshi', 'hokkaido']]
Transformed data (sparse matrix):
  (0, 1)	1.0
  (0, 6)	1.0
  (1, 0)	1.0
  (1, 3)	1.0
  (2, 1)	1.0
  (2, 4)	1.0
  (3, 1)	1.0
  (3, 7)	1.0
  (4, 2)	1.0
  (4, 5)	1.0
Transformed data:
[[0. 1. 0. 0. 0. 0. 1. 0.]
 [1. 0. 0. 1. 0. 0. 0. 0.]
 [0. 1. 0. 0. 1. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 1.]
 [0. 0. 1. 0. 0. 1. 0. 0.]]

カテゴリ名と one hot 表現(列数)の対応は、categories_ からアクセスして確認できる。

In [6]:
print(enc.categories_)
[array(['akitakomachi', 'koshihikari', 'nanatsuboshi'], dtype=object), array(['akita', 'chiba', 'hokkaido', 'niigata', 'toyama'], dtype=object)]
In [7]:
import sklearn.preprocessing

x1 = ['nanatsuboshi', 'akitakomachi', 'koshihikari']
x2 = ['hokkaido', 'akita', 'niigata', 'chiba', 'toyama']

enc = sklearn.preprocessing.OneHotEncoder(categories=[x1, x2])

# numpy 配列にするとエラーがでるようなので 2 次元リストを利用する
x = [['koshihikari',  'niigata'],
     ['akitakomachi', 'akita'],
     ['koshihikari',  'chiba'],
     ['koshihikari',  'toyama'],
     ['nanatsuboshi', 'hokkaido']]

enc.fit(x)
x_enc = enc.transform(x)
x_enc = x_enc.toarray()

print('Transformed data:')
print(x_enc)
Transformed data:
[[0. 0. 1. 0. 0. 1. 0. 0.]
 [0. 1. 0. 0. 1. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 1. 0.]
 [0. 0. 1. 0. 0. 0. 0. 1.]
 [1. 0. 0. 1. 0. 0. 0. 0.]]

One hot 表現に変換するとき、最初の特徴量を取り除くという dummy encoding を行う場合は、次のようにする。

In [8]:
import sklearn.preprocessing

x1 = ['nanatsuboshi', 'akitakomachi', 'koshihikari']
x2 = ['hokkaido', 'akita', 'niigata', 'chiba', 'toyama']

enc = sklearn.preprocessing.OneHotEncoder(categories=[x1, x2], drop='first')

# numpy 配列にするとエラーがでるようなので 2 次元リストを利用する
x = [['koshihikari',  'niigata'],
     ['akitakomachi', 'akita'],
     ['koshihikari',  'chiba'],
     ['koshihikari',  'toyama'],
     ['nanatsuboshi', 'hokkaido']]

enc.fit(x)
x_enc = enc.transform(x)
x_enc = x_enc.toarray()
print('Transformed data:')
print(x_enc)
Transformed data:
[[0. 1. 0. 1. 0. 0.]
 [1. 0. 1. 0. 0. 0.]
 [0. 1. 0. 0. 1. 0.]
 [0. 1. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0.]]

特徴量全体にいくつかの区画を設けて離散化する場合は KBinsDiscretizer を使用する。

In [9]:
import sklearn.preprocessing

x = np.array([[21.1, 16.3],
              [20.6, 12.1],
              [21.4, 13.2],
              [20.7,  9.9],
              [22.3, 11.1],
              [23.5, 10.8],
              [20.3, 10.2]])

est = sklearn.preprocessing.KBinsDiscretizer(n_bins=[3, 2], encode='ordinal').fit(x)
est.fit(x)

x_est = est.transform(x)
print(x_est)
[[1. 1.]
 [0. 1.]
 [2. 1.]
 [1. 0.]
 [2. 1.]
 [2. 0.]
 [0. 0.]]

特徴量の 2 値化は Binarizer を使用する。このとき、閾値を設けて、閾値を超えたら 1、そうでない場合は 0 にするような 2 値化が行われる。

In [10]:
import sklearn.preprocessing

x = np.array([[21.1, 16.3],
              [20.6, 12.1],
              [21.4, 13.2],
              [20.7,  9.9],
              [22.3, 11.1],
              [23.5, 10.8],
              [20.3, 10.2]])

est = sklearn.preprocessing.Binarizer(threshold=[21.0, 11.1]).fit(x)
est.fit(x)

x_est = est.transform(x)
print(x_est)
[[1. 1.]
 [0. 1.]
 [1. 1.]
 [0. 0.]
 [1. 0.]
 [1. 0.]
 [0. 0.]]

評価指標

解析者がテストデータセットの予測結果と教師ラベルを比べて、TP、TN、FP、FN を計算し、定義式に基づいて precision や recall などを算出できるが、scikit-learn が提供する sklearn.metrics モジュールを利用する便利である。関数 1 つで、precision や recall などを計算できるようになり、間違いも回避できる。

以下は、ロジスティック回帰で分類モデルを構築し、様々な評価指標を計算する例を示している。

In [11]:
import sklearn.model_selection
import sklearn.linear_model

X_subset = X[:, 0:2]
X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X_subset, y, test_size=0.4, random_state=2020)

clf = sklearn.linear_model.LogisticRegression(penalty='l2', C=0.01, multi_class='ovr', solver='lbfgs')
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)

評価指標のほとんどは sklearn.metrics モジュールに実装されている。このモジュール中の関数を使って、色々な指標を計算してみる。

In [12]:
import sklearn.metrics


confusion_matrix = sklearn.metrics.confusion_matrix(y_test, y_pred)
print('confusion matrix')
print(confusion_matrix)

accuracy = sklearn.metrics.accuracy_score(y_test, y_pred)
print('accuracy:', accuracy)

f1 = sklearn.metrics.f1_score(y_test, y_pred)
print('f1:', f1)

precision = sklearn.metrics.precision_score(y_test, y_pred)
print('precision:', precision)

recall = sklearn.metrics.recall_score(y_test, y_pred)
print('recall:', recall)
confusion matrix
[[ 70  21]
 [  6 131]]
accuracy: 0.881578947368421
f1: 0.9065743944636678
precision: 0.8618421052631579
recall: 0.9562043795620438

ROC 曲線や PR 曲線を描いたり、曲線下の面積(AUC)を計算したりする場合は、予測結果としてスコアを使用する必要がある。ここで、ロジスティック回帰を利用した予測で、clf.predict 関数で予測ラベルを出力するのではなく、clf.predict_proba 関数で予測確率を出力させて、その確率をスコアと皆して ROC 曲線の座標を計算したり、AUC を計算したりしてみる。

In [13]:
# 確率を予測する
y_prob_pred = clf.predict_proba(X_test)
print(y_prob_pred[1:10, :])
[[0.09388112 0.90611888]
 [0.13324523 0.86675477]
 [0.81951195 0.18048805]
 [0.19601713 0.80398287]
 [0.33975558 0.66024442]
 [0.95980716 0.04019284]
 [0.54227292 0.45772708]
 [0.84220303 0.15779697]
 [0.91263024 0.08736976]]
In [14]:
# class-0 の予測確率は 1 列目、class-1 の予測確率は 2 列目なので、y_prob_pred[:, 1] をスコアとして使用
# 1 を positive と皆したいので、pos_label=1 を指定
fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_test, y_prob_pred[:, 1], pos_label=1)
print('fpr', fpr)
print('tpr', tpr)
print('thresholds', thresholds)

auc = sklearn.metrics.auc(fpr, tpr)
print('auc:', auc)


# ROC-AUC を計算するだけならば class-1 になる確率を `roc_auc_score` 関数に与えるだけで十分 
auc = sklearn.metrics.roc_auc_score(y_test, y_prob_pred[:, 1])
print('auc:', auc)
fpr [0.         0.         0.         0.01098901 0.01098901 0.02197802
 0.02197802 0.03296703 0.03296703 0.04395604 0.04395604 0.05494505
 0.05494505 0.08791209 0.08791209 0.0989011  0.0989011  0.12087912
 0.12087912 0.13186813 0.13186813 0.14285714 0.14285714 0.15384615
 0.15384615 0.1978022  0.1978022  0.23076923 0.23076923 0.24175824
 0.24175824 0.25274725 0.25274725 0.28571429 0.28571429 1.        ]
tpr [0.         0.00729927 0.45255474 0.45255474 0.47445255 0.47445255
 0.48905109 0.48905109 0.74452555 0.74452555 0.7810219  0.7810219
 0.79562044 0.79562044 0.80291971 0.80291971 0.81021898 0.81021898
 0.82481752 0.82481752 0.86131387 0.86131387 0.88321168 0.88321168
 0.93430657 0.93430657 0.94160584 0.94160584 0.97810219 0.97810219
 0.98540146 0.98540146 0.99270073 0.99270073 1.         1.        ]
thresholds [1.98893962e+00 9.88939620e-01 9.05110857e-01 9.02146860e-01
 8.99469864e-01 8.94219008e-01 8.91388243e-01 8.91229023e-01
 7.92515766e-01 7.91245880e-01 7.62453429e-01 7.59521789e-01
 7.50073054e-01 7.43062072e-01 7.39093778e-01 7.36806915e-01
 7.28442976e-01 7.22929661e-01 7.06741040e-01 7.06129192e-01
 6.87004920e-01 6.84176702e-01 6.60244420e-01 6.51668035e-01
 5.85077413e-01 5.63391795e-01 5.57756748e-01 5.37966572e-01
 4.77948355e-01 4.73018466e-01 4.57961920e-01 4.57727078e-01
 4.51212184e-01 4.26706750e-01 4.19443753e-01 6.35668878e-04]
auc: 0.9541188738269031
auc: 0.9541188738269031

上で計算した ROC の座標(TPR および FPR)を利用して、ROC 曲線を描いてみよう。

In [15]:
import matplotlib.pyplot as plt

# 
# ROC 曲線
#

ROC 曲線と同様に、PR 曲線を描いてみよう。PR 曲線の座標を計算する関数の名前がわからない場合は、インターネットなどを活用しましょう。

In [16]:
# PR 曲線をプロットし、AUC を計算する
#
#
#

k 交差検証

モデルのハイパーパラメーターを探索するときによく使われる検証方法である。訓練データセットをさらに k 分割して、k 回の学習と検証を繰り返して、その検証結果の平均を評価指標としているので、より汎化性能の高いモデルが得られると期待できる。

scikit-learn で k 交差検証を行う時、データセットを k 分割してそれを for 文で回して解析者自身が学習と検証を制御する方法と分割数を指定して scikit-learn に自動的に交差検証を行う方法の 2 通りがある。解析者自身で学習と検証を行う場合は次のようにする。なお、ここではパイプラインを作成し、そのパイプラインに対して k 交差検証でハイパーパラメーターの探索を行なってみる。

In [17]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.linear_model
import sklearn.pipeline

X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)


# 訓練データセットを k 分割する
skf = sklearn.model_selection.StratifiedKFold(n_splits=10, random_state=2020, shuffle=True)
#kf = sklearn.model_selection.KFold(n_splits=3, random_state=2020, shuffle=True)

# for 文で各ハイパーパラメーターで評価する
for c in [0.001, 0.01, 0.1, 1.0, 10, 100]:
    scores = []
    
    # for 文で学習と評価を繰り返す
    for train_index, test_index in skf.split(X_train, y_train):
        pipe = sklearn.pipeline.Pipeline([
            ('scaler', sklearn.preprocessing.StandardScaler()),
            ('clf', sklearn.svm.SVC(kernel='linear', C=c))
        ])
        pipe.fit(X_train, y_train)
        y_pred = pipe.predict(X_test)
        _score = sklearn.metrics.accuracy_score(y_test, y_pred)
        scores.append(_score)

    print(c, sum(scores)/len(scores))
0.001 0.9385964912280702
0.01 0.9824561403508769
0.1 0.9780701754385965
1.0 0.9736842105263157
10 0.9692982456140349
100 0.9692982456140349

分割数を指定して、scikit-learn で(for 文を使わずに)自動的に交差検証を繰り返す場合は、cross_val_scores または cross_validate 関数を使用する。後者の方が新しく追加された関数であり、複数の評価指標でモデル検証を行えるなどの機能がある。ここでは後者の cross_validate 関数の使い方を紹介する。

In [18]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.linear_model
import sklearn.pipeline

X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)

# 訓練データセットを k 分割する
skf = sklearn.model_selection.StratifiedKFold(n_splits=10, random_state=2020, shuffle=True)
#kf = sklearn.model_selection.KFold(n_splits=3, random_state=2020, shuffle=True)

# 評価指標
scoring = ['f1_macro', 'precision_macro', 'recall_macro']


# for 文で各ハイパーパラメーターで評価する
for c in [0.001, 0.01, 0.1, 1.0, 10, 100]:
    scores = {
        'test_f1_macro': [],
        'test_precision_macro': [],
        'test_recall_macro': []
    }
    
    pipe = sklearn.pipeline.Pipeline([
            ('scaler', sklearn.preprocessing.StandardScaler()),
            ('clf', sklearn.svm.SVC(kernel='linear', C=c))
        ])
    
    # cv=10 のように整数値を代入してもよい
    score = sklearn.model_selection.cross_validate(pipe, X_train, y_train, scoring=scoring, cv=skf)
    
    # 10-cv の描く指標の平均値を計算する
    for score_key in ['test_f1_macro', 'test_precision_macro', 'test_recall_macro']:
        scores[score_key] = np.mean(score[score_key])
    
    
    print(c, scores)
0.001 {'test_f1_macro': 0.901965451259651, 'test_precision_macro': 0.9452819274123619, 'test_recall_macro': 0.8839743589743589}
0.01 {'test_f1_macro': 0.9497720184533321, 'test_precision_macro': 0.966705533596838, 'test_recall_macro': 0.9397144522144523}
0.1 {'test_f1_macro': 0.9740865071340299, 'test_precision_macro': 0.9790571747093486, 'test_recall_macro': 0.9704545454545455}
1.0 {'test_f1_macro': 0.9709580520404584, 'test_precision_macro': 0.9752734584256322, 'test_recall_macro': 0.9681818181818181}
10 {'test_f1_macro': 0.9454514858245899, 'test_precision_macro': 0.9506394330307375, 'test_recall_macro': 0.9443181818181816}
100 {'test_f1_macro': 0.9486226974351342, 'test_precision_macro': 0.9540662598271294, 'test_recall_macro': 0.946590909090909}

データセットを分割する関数として StratifiedKFold のほかに KFoldShuffleSplit などのデータセット分割関数が用意されている。これらの関数の動作の違いについてでは、scikit-learn のウェブサイトで、画像付きで解説されている。詳細に知りたい方は下記のウェブページを参照してください。

https://scikit-learn.org/stable/modules/cross_validation.html
関連する日本語記事: https://qiita.com/Hatomugi/items/620c1bc757266b00e87f

ここで練習用に、マクロ F1 を評価指標として、上で定義した pipe パイプラインに対して、最適なハイパーパラメーター C を探してみよう。最適な C を探す時、最初は粗めに C の候補を決めて、交差検証を行う。

In [19]:
## 以下例、必ずしもこの通りにする必要はない
##
## for c in [0.001, 0.01, 0.1, 1, 10, 100, 1000]
##   交差検証で評価する
##
## 0.1 のときの性能が最大であれば、次の for 文を次のようにする
##
## for c in [0.01, 0.05, 0.1, 0.15, 0.2, ...., 0.95, 1.0]
##
##
## このように範囲を徐々に縮めていく
##

バイアスとバリアンスの評価

学習曲線および検証曲線を用いることで、モデルの学習不足や過学習などを評価することができる。ここで、学習曲線および検証曲線を描く例を示す。

学習曲線は横軸に訓練データのサンプル数、縦軸に評価指標をプロットした折れ線グラフである。for 文を使用して、サンプル数を調整しながら、学習と評価を繰り返してもよいが、ここでは scikit-learn が提供している learning_curve 関数を使用する。

In [20]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.pipeline


# 最初の 5 特徴量で学習曲線をプロットする
X_subset = X[:, 0:5]
X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X_subset, y, test_size=0.4, random_state=2020)


# 標準化を行ってから分類を行うパイプラインを構築
pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear', C=1))
       ])


# 学習と検証を繰り返して、学習曲線をプロットするための座標を計算
train_sizes, train_scores, test_scores =\
        sklearn.model_selection.learning_curve(estimator=pipe,
                                               X=X_train,
                                               y=y_train,
                                               train_sizes=np.linspace(0.1, 1.0, 21),
                                               cv=10,
                                               scoring='accuracy')

# 10-cv の平均値と標準偏差を計算
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)

# プロット
plt.plot(train_sizes, train_scores_mean, color='orange', marker='o', markersize=5, label='training accuracy')
plt.fill_between(train_sizes, train_scores_mean + train_scores_std, train_scores_mean - train_scores_std,
                 alpha=0.2, color='orange')
plt.plot(train_sizes, test_scores_mean, color='darkblue', marker='o', markersize=5, label='validation accuracy')
plt.fill_between(train_sizes, test_scores_mean + test_scores_std, test_scores_mean - test_scores_std,
                 alpha=0.2, color='darkblue')

plt.xlabel('#training samples')
plt.ylabel('accuracy')
plt.legend(loc='lower right')
plt.show()

特徴量を少しずつ増やしたとき、学習曲線がどのように変化していくのかを調べてみよう。特徴量が少ないときは、training accuracy と validation accuracy のどちらも低い。特徴量が多い時は、training accuracy はほぼ 1.0 のままで、validation accuracy も 1.0 に近い値を取るようになる。

In [21]:
# use for-loop to check the changes of accuracy curve

## n_features = X.shape[1]
## for i in range(2, n_features):
##   plot learning-curve with i features
##

検証曲線もモデルの学習不足や過学習に使用できる。検証曲線の場合は validation_curve 関数を使用すると便利である。

In [22]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.pipeline


# 最初の 5 特徴量で学習曲線をプロットする
X_subset = X[:, 0:5]
X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X_subset, y, test_size=0.4, random_state=2020)


pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])



# 学習と検証を繰り返して、検証曲線をプロットするための座標を計算
svc_c_range = 10 ** np.linspace(-5, 2, 10)
print(svc_c_range)

train_scores, test_scores =\
        sklearn.model_selection.validation_curve(estimator=pipe,
                                                 X=X_train,
                                                 y=y_train,
                                                 param_name='clf__C',
                                                 param_range=svc_c_range,
                                                 cv=10,
                                                 scoring='accuracy')

# 10-cv の平均値と標準偏差を計算
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std  = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std  = np.std(test_scores, axis=1)


# プロット
plt.plot(svc_c_range, train_scores_mean, color='orange', marker='o', markersize=5, label='training accuracy')
plt.fill_between(svc_c_range, train_scores_mean + train_scores_std, train_scores_mean - train_scores_std,
                 alpha=0.2, color='orange')
plt.plot(svc_c_range, test_scores_mean, color='darkblue', marker='o', markersize=5, label='validation accuracy')
plt.fill_between(svc_c_range, test_scores_mean + test_scores_std, test_scores_mean - test_scores_std,
                 alpha=0.2, color='darkblue')

plt.xlabel('C')
plt.ylabel('accuracy')
plt.legend(loc='lower right')
plt.xscale('log')
plt.show()
[1.00000000e-05 5.99484250e-05 3.59381366e-04 2.15443469e-03
 1.29154967e-02 7.74263683e-02 4.64158883e-01 2.78255940e+00
 1.66810054e+01 1.00000000e+02]

特徴量を少しずつ増やしたとき、検証曲線がどのように変化していくのかを調べてみよう。特徴量が少ないときは、training accuracy と validation accuracy のどちらも低い。特徴量が多い時は、training accuracy はほぼ 1.0 のままで、validation accuracy も 1.0 に近い値を取るようになる。

In [23]:
# use for-loop to check the changes of accuracy curve

## n_features = X.shape[1]
## for i in range(2, n_features):
##   plot validation-curve with i features
##

余裕があれば、サンプル数が多い時と少ない時、特徴量が多い時と少ない時の 4 通りの組み合わせで検証曲線をプロットして、サンプルの数・特徴量の数がモデルの性能に及ぼす影響を考察してみましょう。

In [24]:
# use for-loop to check the changes of accuracy curve

## n_features = [2, 30] # 自由に決めてよい
## n_samples = [50, 569] # 自由に決めてよい
## for each sample_set
##   for each feature_set
##     plot validation-curve with i samples x j features
##

ハイパーパラメーター探索

最適なハイパーパラメーターを探し出すために、ハイパーパラメーターの候補となる値を複数用意し、すべての候補に対して交差検証を行えばよい。ハイパーパラメーターが 1 つだけあるとき for 文を利用したループでも十分にパラメーターの探索を行うことが可能。しかし、ハイパーパラメーターが複数になると、for 文を複数入れ子構造にする必要が生じてくる。このままでは不便であるから、scikit-learn で用意されている GridSearchCVRandomizedSearchCV 関数を使用すると便利。

まずは理解しやすいグリッドサーチを行う GridSearchCV を行う例を示す。

In [25]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.pipeline


X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)


pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])

parameters = {
    'clf__kernel': ('linear', 'rbf'),
    'clf__C': 10**np.linspace(-5, 2, 10)
}


gs = sklearn.model_selection.GridSearchCV(pipe, parameters, scoring='f1', cv=10)
gs.fit(X_train, y_train)
Out[25]:
GridSearchCV(cv=10, error_score='raise-deprecating',
             estimator=Pipeline(memory=None,
                                steps=[('scaler',
                                        StandardScaler(copy=True,
                                                       with_mean=True,
                                                       with_std=True)),
                                       ('clf',
                                        SVC(C=1.0, cache_size=200,
                                            class_weight=None, coef0=0.0,
                                            decision_function_shape='ovr',
                                            degree=3, gamma='auto_deprecated',
                                            kernel='linear', max_iter=-1,
                                            probability=False,
                                            random_state=None, shrinkin...
                                            tol=0.001, verbose=False))],
                                verbose=False),
             iid='warn', n_jobs=None,
             param_grid={'clf__C': array([1.00000000e-05, 5.99484250e-05, 3.59381366e-04, 2.15443469e-03,
       1.29154967e-02, 7.74263683e-02, 4.64158883e-01, 2.78255940e+00,
       1.66810054e+01, 1.00000000e+02]),
                         'clf__kernel': ('linear', 'rbf')},
             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
             scoring='f1', verbose=0)

グリッドサーチの結果が gs インスタンスに保存されている。性能の最も高いモデルは best_estimator_ に保存され、そのパラメーターは best_params_ に保存され、そのときのスコアは best_score_ に保存される。

In [26]:
best_params = gs.best_params_
best_score = gs.best_score_

print(best_params)
print(best_score)
{'clf__C': 0.0774263682681127, 'clf__kernel': 'linear'}
0.9799934832192898
In [27]:
clf = gs.best_estimator_

# テストデータセットを使って最終評価
y_pred = clf.predict(X_test)
score = sklearn.metrics.f1_score(y_test, y_pred)
print(score)
0.9819494584837546

ハイパーパラメーターのすべての組み合わせを評価した結果は cv_results_ にディクショナリの形で保存されている。そのまま表示させるとみづらいので、これをデータフレームに変換して表示してみる。最初の 4 列は、各ハイパーパラメーターの組み合わせ時における 10-cv の学習時間の平均と標準偏差、テスト時間の平均と標準偏差である。

In [28]:
print(pd.DataFrame(gs.cv_results_))
    mean_fit_time  std_fit_time  mean_score_time  std_score_time param_clf__C  \
0        0.004827      0.001668         0.001478        0.000526        1e-05   
1        0.004118      0.000287         0.001156        0.000325        1e-05   
2        0.003315      0.000433         0.000996        0.000107  5.99484e-05   
3        0.004131      0.000194         0.001009        0.000066  5.99484e-05   
4        0.002854      0.000045         0.000861        0.000008  0.000359381   
5        0.004351      0.000081         0.001012        0.000045  0.000359381   
6        0.002193      0.000028         0.000819        0.000044   0.00215443   
7        0.004349      0.000236         0.001004        0.000068   0.00215443   
8        0.001806      0.000207         0.000772        0.000039    0.0129155   
9        0.004057      0.000061         0.000969        0.000017    0.0129155   
10       0.001670      0.000117         0.000742        0.000034    0.0774264   
11       0.003422      0.000130         0.000920        0.000036    0.0774264   
12       0.001778      0.000101         0.000715        0.000017     0.464159   
13       0.002455      0.000061         0.000842        0.000052     0.464159   
14       0.002037      0.000167         0.000756        0.000071      2.78256   
15       0.002232      0.000108         0.000782        0.000018      2.78256   
16       0.002176      0.000183         0.000712        0.000019       16.681   
17       0.002165      0.000053         0.000762        0.000014       16.681   
18       0.002281      0.000239         0.000710        0.000021          100   
19       0.002151      0.000058         0.000758        0.000022          100   

   param_clf__kernel                                             params  \
0             linear         {'clf__C': 1e-05, 'clf__kernel': 'linear'}   
1                rbf            {'clf__C': 1e-05, 'clf__kernel': 'rbf'}   
2             linear  {'clf__C': 5.994842503189409e-05, 'clf__kernel...   
3                rbf  {'clf__C': 5.994842503189409e-05, 'clf__kernel...   
4             linear  {'clf__C': 0.00035938136638046257, 'clf__kerne...   
5                rbf  {'clf__C': 0.00035938136638046257, 'clf__kerne...   
6             linear  {'clf__C': 0.0021544346900318843, 'clf__kernel...   
7                rbf  {'clf__C': 0.0021544346900318843, 'clf__kernel...   
8             linear  {'clf__C': 0.01291549665014884, 'clf__kernel':...   
9                rbf  {'clf__C': 0.01291549665014884, 'clf__kernel':...   
10            linear  {'clf__C': 0.0774263682681127, 'clf__kernel': ...   
11               rbf  {'clf__C': 0.0774263682681127, 'clf__kernel': ...   
12            linear  {'clf__C': 0.4641588833612782, 'clf__kernel': ...   
13               rbf  {'clf__C': 0.4641588833612782, 'clf__kernel': ...   
14            linear  {'clf__C': 2.782559402207126, 'clf__kernel': '...   
15               rbf  {'clf__C': 2.782559402207126, 'clf__kernel': '...   
16            linear  {'clf__C': 16.68100537200059, 'clf__kernel': '...   
17               rbf  {'clf__C': 16.68100537200059, 'clf__kernel': '...   
18            linear         {'clf__C': 100.0, 'clf__kernel': 'linear'}   
19               rbf            {'clf__C': 100.0, 'clf__kernel': 'rbf'}   

    split0_test_score  split1_test_score  split2_test_score  \
0            0.771930           0.785714           0.785714   
1            0.771930           0.785714           0.785714   
2            0.771930           0.785714           0.785714   
3            0.771930           0.785714           0.785714   
4            0.846154           0.916667           0.897959   
5            0.771930           0.785714           0.785714   
6            0.956522           0.954545           0.936170   
7            0.771930           0.785714           0.785714   
8            0.956522           1.000000           0.913043   
9            0.771930           0.785714           0.785714   
10           0.977778           1.000000           0.888889   
11           0.933333           0.930233           0.933333   
12           0.954545           0.977778           0.909091   
13           0.954545           1.000000           0.933333   
14           0.977778           0.956522           0.888889   
15           0.954545           1.000000           0.909091   
16           0.977778           0.977778           0.888889   
17           0.930233           1.000000           0.909091   
18           0.977778           0.977778           0.888889   
19           0.930233           1.000000           0.888889   

    split3_test_score  split4_test_score  split5_test_score  \
0            0.785714           0.785714           0.785714   
1            0.785714           0.785714           0.785714   
2            0.785714           0.785714           0.785714   
3            0.785714           0.785714           0.785714   
4            0.846154           0.846154           0.846154   
5            0.785714           0.785714           0.785714   
6            0.916667           0.977778           0.916667   
7            0.785714           0.785714           0.785714   
8            0.956522           0.977778           0.916667   
9            0.785714           0.785714           0.785714   
10           1.000000           0.977778           1.000000   
11           0.916667           0.977778           0.916667   
12           0.976744           0.977778           1.000000   
13           0.977778           0.977778           0.956522   
14           0.976744           0.977778           1.000000   
15           1.000000           0.977778           1.000000   
16           0.954545           0.977778           0.977778   
17           0.977778           0.977778           1.000000   
18           0.954545           0.977778           0.977778   
19           0.977778           0.977778           1.000000   

    split6_test_score  split7_test_score  split8_test_score  \
0            0.785714           0.785714           0.785714   
1            0.785714           0.785714           0.785714   
2            0.785714           0.785714           0.785714   
3            0.785714           0.785714           0.785714   
4            0.916667           0.897959           0.846154   
5            0.785714           0.785714           0.785714   
6            1.000000           0.956522           0.936170   
7            0.785714           0.785714           0.785714   
8            1.000000           0.977778           0.977778   
9            0.785714           0.785714           0.785714   
10           1.000000           0.977778           1.000000   
11           0.976744           0.956522           0.956522   
12           1.000000           0.954545           1.000000   
13           1.000000           0.977778           0.977778   
14           0.976744           0.954545           1.000000   
15           1.000000           0.977778           0.977778   
16           0.976744           0.954545           0.977778   
17           0.952381           0.954545           1.000000   
18           0.952381           0.954545           0.977778   
19           0.952381           0.954545           1.000000   

    split9_test_score  mean_test_score  std_test_score  rank_test_score  
0            0.785714         0.784299        0.004183               14  
1            0.785714         0.784299        0.004183               14  
2            0.785714         0.784299        0.004183               14  
3            0.785714         0.784299        0.004183               14  
4            0.916667         0.877576        0.032172               13  
5            0.785714         0.784299        0.004183               14  
6            0.977778         0.952892        0.025790               11  
7            0.785714         0.784299        0.004183               14  
8            0.977778         0.965360        0.028738                8  
9            0.785714         0.784299        0.004183               14  
10           0.977778         0.979993        0.032079                1  
11           0.977778         0.947516        0.023329               12  
12           1.000000         0.974988        0.027729                4  
13           1.000000         0.975490        0.020973                3  
14           1.000000         0.970920        0.031426                5  
15           0.976744         0.977304        0.026882                2  
16           0.976744         0.964076        0.026607                9  
17           0.976744         0.967745        0.029457                6  
18           1.000000         0.963966        0.028604               10  
19           0.976744         0.965730        0.033773                7  

SVM のようにカーネル関数が異なるとハイパーパラメーターをチューニングする必要がある。例えば、多項式カーネルのハイパーパラメーターは degree, gamma, coef0 があり、RBF カーネルのハイパーパラメーターは gamma、シグモイドカーネルのハイパーパラメーターは gamma, coef0 がある。そのため、次のようなグリッドサーチで、総当たりでのアプローチは効率が悪い。つまり、次のように書くとカーネル関数が RBF のときでも、degree を一つずつ動かして検証することになるので、その分だけ無駄になる。

In [29]:
pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])

parameters = {
    'clf__C': 10**np.linspace(-5, 2, 10),
    'clf__kernel': ('linear', 'rbf'),
    'clf__degree': (2, 3, 4, 5, 6, 7),
    'clf__gamma': 10**np.linspace(-5, 2, 10),
    'clf__coef0': np.linspace(-10, 10, 10)
}

gs = sklearn.model_selection.GridSearchCV(pipe, parameters, scoring='f1', cv=10)

ここで、次のように、ハイパーパラメーターの組み合わせをいくつかのパターンにまとめて、与えると効率よくグリッドサーチができるようになる。

import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.pipeline


X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)


pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])

parameters = [
        {
            'clf__kernel': ['linear'],
            'clf__C': 10**np.linspace(-5, 2, 3),
        },
        {
            'clf__kernel': ['rbf'],
            'clf__C': 10 ** np.linspace(-5, 5, 3),
            'clf__gamma': 10**np.linspace(-5, 2, 3),      
            'clf__degree': 10 ** np.linspace(-5, 5, 3),
            'clf__coef0': np.linspace(-10, 10, 3)
        },
        {
            'clf__kernel': ['rbf'],
            'clf__C': 10**np.linspace(-5, 2, 3),
            'clf__gamma': 10**np.linspace(-5, 2, 3)
        },
        {
            'clf__kernel': ['sigmoid'],
            'clf__C': 10**np.linspace(-5, 2, 3),
            'clf__gamma': 10**np.linspace(-5, 2, 3),
            'clf__coef': np.linspace(-10, 10, 3)
        }
    ]


gs = sklearn.model_selection.GridSearchCV(pipe, parameters, scoring='f1', cv=10)
gs.fit(X_train, y_train)

# 最適なモデルを取得
clf = gs.best_estimator_
best_params = gs.best_params_
best_score = gs.best_score_
print(best_params)
print(best_score)

# テストデータセットを使って最終評価
y_pred = clf.predict(X_test)
score = sklearn.metrics.f1_score(y_test, y_pred)
print(score)

ランダムサーチもグリッドサーチと同じ要領で実行できる。また、ランダムサーチの出力結果もグリッドサーチと同様に扱える。ただし、ハイパーパラメーターの候補は、確率分布の形で与える。

In [30]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.pipeline
import scipy

X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)


pipe = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])

parameters = {
    'clf__kernel': ('linear', 'rbf'),
    'clf__C': scipy.stats.uniform(loc=0, scale=4)
}


gs = sklearn.model_selection.RandomizedSearchCV(pipe, parameters, scoring='f1', cv=10)
gs.fit(X_train, y_train)

best_params = gs.best_params_
best_score = gs.best_score_

print(best_params)
print(best_score)

clf = gs.best_estimator_

# テストデータセットを使って最終評価
y_pred = clf.predict(X_test)
score = sklearn.metrics.f1_score(y_test, y_pred)
print(score)

# 各ハイパーパラメーターの組み合わせおよびその時のスコア
print(pd.DataFrame(gs.cv_results_))
{'clf__C': 0.7764314250110171, 'clf__kernel': 'rbf'}
0.977608933913919
0.9818181818181817
   mean_fit_time  std_fit_time  mean_score_time  std_score_time param_clf__C  \
0       0.005447      0.001287         0.002142        0.000856        1.186   
1       0.003687      0.001035         0.001342        0.000473      2.69058   
2       0.002384      0.000097         0.000816        0.000028     0.776431   
3       0.002401      0.000280         0.000807        0.000043      1.82615   
4       0.002686      0.000046         0.000839        0.000013     0.244448   
5       0.002269      0.000064         0.000836        0.000090      1.33174   
6       0.002393      0.000033         0.000805        0.000006     0.538636   
7       0.002295      0.000056         0.000784        0.000005      1.24842   
8       0.001871      0.000130         0.000713        0.000017      1.43785   
9       0.002340      0.000154         0.000823        0.000094      1.15858   

  param_clf__kernel                                             params  \
0               rbf  {'clf__C': 1.1860010635035994, 'clf__kernel': ...   
1               rbf  {'clf__C': 2.69057584870411, 'clf__kernel': 'r...   
2               rbf  {'clf__C': 0.7764314250110171, 'clf__kernel': ...   
3               rbf  {'clf__C': 1.8261509275738463, 'clf__kernel': ...   
4               rbf  {'clf__C': 0.2444481402392209, 'clf__kernel': ...   
5               rbf  {'clf__C': 1.3317421627689678, 'clf__kernel': ...   
6               rbf  {'clf__C': 0.538636493898685, 'clf__kernel': '...   
7               rbf  {'clf__C': 1.2484166242515191, 'clf__kernel': ...   
8            linear  {'clf__C': 1.437846903580437, 'clf__kernel': '...   
9               rbf  {'clf__C': 1.1585781277267695, 'clf__kernel': ...   

   split0_test_score  split1_test_score  split2_test_score  split3_test_score  \
0           0.954545           1.000000           0.909091           1.000000   
1           0.954545           1.000000           0.909091           1.000000   
2           0.954545           1.000000           0.933333           0.977778   
3           0.954545           1.000000           0.909091           1.000000   
4           0.933333           0.954545           0.933333           0.916667   
5           0.954545           1.000000           0.909091           1.000000   
6           0.954545           1.000000           0.933333           0.977778   
7           0.954545           1.000000           0.909091           1.000000   
8           0.954545           0.956522           0.888889           0.976744   
9           0.954545           1.000000           0.909091           0.977778   

   split4_test_score  split5_test_score  split6_test_score  split7_test_score  \
0           0.977778           0.977778           1.000000           0.977778   
1           0.977778           1.000000           1.000000           0.977778   
2           0.977778           0.977778           1.000000           0.977778   
3           0.977778           1.000000           1.000000           0.977778   
4           0.977778           0.936170           1.000000           0.977778   
5           0.977778           1.000000           1.000000           0.977778   
6           0.977778           0.977778           1.000000           0.977778   
7           0.977778           0.977778           1.000000           0.977778   
8           0.977778           1.000000           0.976744           0.954545   
9           0.977778           0.977778           1.000000           0.977778   

   split8_test_score  split9_test_score  mean_test_score  std_test_score  \
0           0.977778           0.976744         0.975089        0.025815   
1           0.977778           0.976744         0.977304        0.026882   
2           0.977778           1.000000         0.977609        0.020001   
3           0.977778           0.976744         0.977304        0.026882   
4           0.977778           0.977778         0.958442        0.025959   
5           0.977778           0.976744         0.977304        0.026882   
6           0.977778           1.000000         0.977609        0.020001   
7           0.977778           0.976744         0.975089        0.025815   
8           1.000000           1.000000         0.968536        0.031695   
9           0.977778           0.976744         0.972873        0.024502   

   rank_test_score  
0                6  
1                3  
2                1  
3                3  
4               10  
5                3  
6                1  
7                6  
8                9  
9                8  

2 種類の分類アルゴリズムを比較するにはネストされた交差検証を行う必要がある。子の交差検証ループで各アルゴリズムの最適なハイパーパラメーター(SVM ならば C、決定木ならば max_depth など)を決定し、親の交差検証ループではアルゴリズム(SVM や決定木など)の性能評価を行う。

In [31]:
import sklearn.model_selection
import sklearn.metrics
import sklearn.svm
import sklearn.tree
import sklearn.pipeline

X_train, X_test, y_train, y_test =\
  sklearn.model_selection.train_test_split(X, y, test_size=0.4, random_state=2020)


pipe_1 = sklearn.pipeline.Pipeline([
         ('scaler', sklearn.preprocessing.StandardScaler()),
         ('clf', sklearn.svm.SVC(kernel='linear'))
       ])
parameters_1 = {
    'clf__kernel': ('linear', 'rbf'),
    'clf__C': 10**np.linspace(-5, 2, 10)
}


pipe_2 = sklearn.pipeline.Pipeline([
               ('clf', sklearn.tree.DecisionTreeClassifier())
           ])
parameters_2 = [
    {'clf__max_depth': [2, 3, 4, 5, 6, 7, 8]}
]


# 子の交差検証ループで最適なハイパーパラメーターを探索
gs_1 = sklearn.model_selection.GridSearchCV(pipe_1, parameters_1, scoring='f1', cv=2)
gs_2 = sklearn.model_selection.GridSearchCV(pipe_2, parameters_2, scoring='f1', cv=2)


# 親の交差検証ループで最適なアルゴリズムを評価
scores_1 = sklearn.model_selection.cross_validate(gs_1, X_train, y_train, scoring='f1', cv=10)
scores_2 = sklearn.model_selection.cross_validate(gs_2, X_train, y_train, scoring='f1', cv=10)


# 評価結果
print('pipeline 1', scores_1['test_score'].mean())
print('pipeline 2', scores_2['test_score'].mean())
pipeline 1 0.9733289415898112
pipeline 2 0.928276171807455

最適なパイプラインは pipe_1 であることがわかったので、このパイプラインにすべての教師データを代入してモデルを構築し、最終評価を行う。

In [32]:
gs_1.fit(X_train, y_train)
clf = gs_1.best_estimator_

# テストデータセットを使って最終評価
y_pred = clf.predict(X_test)
score = sklearn.metrics.f1_score(y_test, y_pred)
print(score)
0.9855072463768116

上の例では子の交差検証ループを GridSearchCV 関数にセットアップし、これらを親の交差検証ループを cross_validate 関数に代入して実行した。このような入れ子構造でネストされた交差検証を可能にし、評価結果も得られる。この結果からパイプライン 1 の方が優れていることはわかった。その後、すべての教師データをパイプライン 1 に代入し、モデルを構築した。

scikit-learn が提供している関数を使用すると、ネストされた交差検証のような複雑な機能を簡易に実現できる。しかし、複雑なモデルを構築する場合、調整が不可能になったりします。では、練習として、上で用いたネストされた交差検証とほぼ同じような機能を果たす交差検証を(cross_validate 関数を使わずに)for 文、KFoldGridSearchCV などの関数で書いてみよう。

In [33]:
##
## for i in 10-cv:
##    GridSearchCV with 2-cv for pipe_1
##    GridSearchCV with 2-cv for pipe_2
## 
## 10-cv 結果から pipe_1 と pipe_2 の評価結果を計算
##

Windows 向け GraphViz

In [34]:
#!/usr/bin/env python
"""
For windows users, install the graphviz with the installer listed below
    https://graphviz.gitlab.io/_pages/Download/Download_windows.html

If you got problems, you may copy the graphviz folder into the following location.
     AppData\Local\Continuum\anaconda3\Lib\site-packages\graphviz 
"""

import os
import subprocess
import graphviz.backend as be
import platform as p
import shutil


os_sys = ["Linux", "Darwin", "Windows"]
cmd = ""
dot_cmd = ['dot','-V']

plf = p.system()
rel = p.release()

print("OS : {}, Ver: {}".format(plf, rel))
print("System path:")

if plf == os_sys[2]:
    cmd = 'dot'
    print( os.getenv('Path') )
elif plf == os_sys[1] or plf == os_sys[0]:
    print( os.getenv('PATH') )
print()
print('Graphviz version: for dot command')
found = shutil.which('dot')
if not found:
    print("Do not found {}".format(cmd))
else:
    print("Run dot command with subprocess")
    proc = subprocess.Popen(dot_cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    (output, err) = proc.communicate()
    proc.stderr.close()
    print(err.decode().strip())
    print()
    print("Run dot command with graphviz backend")
    stdout, stderr = be.run(dot_cmd, capture_output=True, check=True, quiet=True)
    print(stderr.decode().strip())
OS : Darwin, Ver: 18.7.0
System path:
/opt/anaconda3/bin:/Users/jsun/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/TeX/texbin:/opt/X11/bin

Graphviz version: for dot command
Run dot command with subprocess
dot - graphviz version 2.40.1 (20161225.0304)

Run dot command with graphviz backend
dot - graphviz version 2.40.1 (20161225.0304)
In [ ]: