スポンサーリンク
交差検証のやり方
交差検証とは
交差検証ってすごく単純です。学習にこれから使うデータをK分割して、K-1個のデータセットを学習に使い残りの1個を検証に使う。ということをK回繰り返す検証方法です。
以下に簡単に図を載せておきます。
この図の場合、5回算出した各評価値を平均したものを、最終的な評価値として使います。この評価値はAccuracyかもしれませんし、Recallかもしれませんし、何やろうとしているかによって自分たちで決めてから評価を行うことになります。
例えば分類というタスクにおいては以下の評価指標が基本的です。
交差検証の実行それ自体はハッキリ言って単純明快です。難しい式も全く必要ありません。しかし、これをやるかやらないかで、まともに性能評価しているか否かが大きく分かれます。もちろん他にも評価方法は沢山あるんですが、どれを使えばいいかわかんないならば、まずはこれを抑えるべきでしょう。
scikit-learnのKFold
交差検証はscikit-learnに入っているKFoldクラスを使うと簡単に実行できます。
また、使う機械学習手法がscikit-learnによる実装でなければいけないわけではなく、例えばTensorFlowやChainerなどで実装された学習コードを、scikit-learnの交差検証でラッピングすることが可能です。
ですから、是非、scikit-learnをメインで使わないにしても、scikit-learnの交差検証のやり方は抑えておきましょう。
例えば以下のコードでは150個のデータを5個に分割して、訓練用と検証用のデータセットを作ってくれているのがわかります。ですから、実際には下記のfor文の中に、何かしらの訓練コードと評価指標を計算するコードが書かれていれば良いというわけです。
また、今回たまたま、学習データを割り切れる分割数にしていたため良かったものの、実は割り切れない数で分割数を指定してしまった場合には以下のようになります。
全体が150個のデータに対して、分割数を8にした場合、検証データが19だったり18だったりするわけです。
また、KFold.spritというメソッドはあくまでIndexを返してくれるだけであることに注意しましょう。引数であるXとyの要素を返してくれるわけではありません。返してくれるのがindexであるおかげで、どのデータが検証用に割り当てられたのかを確認することもできます。以下では150個のデータを15分割した場合に、どのindexに対応するデータが検証用に割り当てられてのかを表示しています。
ちゃんと被りなく、15回分の検証用データが作成されていそうですね。今、KFlodのインスタンスを作る際にshuffle=Trueを渡しているため、indexをランダムに15分割して取ってくるようになっています。一方で、shuffle=Falseにしておけば以下のように前から順番に検証用データを取ってくるようになります。
今回のIrisデータというのは、分類問題で各クラスのデータが順番に格納されているため、検証データをランダムに取ってくる設定にしておかないと、最初の方はクラス1に属するデータばかりが検証データに回されることになり、明らかに変な偏りを持った評価が行われることになってしまいます。
何か特別な狙いが無い限りは、検証データには均等にデータが入るようにしたほうが良いでしょう(もちろん訓練データの方も均等が望ましい)。
交差検証のサンプル(Keras)
Kerasだと学習がfitだけで済むので、今回はとりあえずKerasを使います。別に他のものでも構いません。
from keras.models import Sequential | |
from keras.layers import Dense, Activation, Dropout | |
import matplotlib.pyplot as plt | |
from sklearn.model_selection import KFold | |
from sklearn.datasets import load_iris | |
import numpy as np | |
iris = load_iris() | |
X = iris.data | |
T = iris.target | |
## モデル構築 | |
model = Sequential() | |
model.add(Dense(128, input_shape=(4,))) | |
model.add((Dropout(0.2))) | |
model.add(Dense(3)) | |
model.add(Activation("softmax")) | |
model.compile(loss="categorical_crossentropy", | |
optimizer="sgd", | |
metrics=['accuracy']) | |
kf = KFold(n_splits=5, shuffle=True) | |
## 5回分のaccuracyの和を入れる(後で割る) | |
sum_accuracy = 0 | |
## 5回分のaccuracyを1つ1つ保存する | |
accuracy = [] | |
## 交差検証 | |
for train_idx, val_idx in kf.split(X=X, y=T): | |
train_x, val_x = X[train_idx], X[val_idx] | |
train_t, val_t = T[train_idx], T[val_idx] | |
##学習 | |
model.fit(train_x, np.eye(3)[train_t], batch_size=16, nb_epoch=100) | |
##評価 | |
predict = model.predict_on_batch(val_x) | |
predict_class = np.argmax(predict, axis=1) | |
acc = np.mean((predict_class==val_t).astype(np.float32)) | |
##評価値をリストに追加 | |
accuracy.append(acc) | |
##今回の評価値を足す | |
sum_accuracy += acc | |
print('accuracy : {}'.format(accuracy)) | |
## 5回分の評価値の平均 | |
sum_accuracy /= 5 | |
print('Kfold accuracy: {}'.format(sum_accuracy)) |
作ったモデルに対して交差検証を行い獲得した平均的な評価値を、今回のモデルに対する評価値とします。その後、違うハイパーパラメータを設定してみて(今回ならば中間層のユニットとドロップアウトが調整できる)、同じように交差検証を行い、評価値を比較していくことになります。
1回の学習に時間が掛からない、そしてハイパーパラメータの数も少ないならば、是非交差検証をしっかり行ってチューニングをしてやるのが良いでしょう。
今回はAccuracy使いましたが、別のものでも良いです(というより、問題に応じて適したものを使うべき)。
ディープラーニングでの検証
交差検証は見ての通り、1つのモデルを評価するためにデータを分割した数だけ学習を行わなければなりません。すなわち、1回の学習に1時間掛かるとすれば、そのモデルを評価し終えるのは5分割したならば5時間になるわけです。
そして、1回の評価(交差検証)で1セットのハイパーパラメータを評価したことになり、ハイパーパラメータの数を考えると途方に暮れてしまいます。ハイパーパラメータを10セットしか評価しないことにしても、これで50時間もの時間を要することになります。
ディープラーニングでの学習時間のことを考えると中々交差検証はできないかもしれません。そこで、検証セットを1つだけ準備しておいて、学習の過程を見張ることにします。訓練データに対する評価指標と、検証データに対する評価指標が剥離する(訓練データばかりに良い評価が得られる)ようであれば、その段階で学習を止めてしまうなどの手法が取られます(early stop)。
ハイパーパラメータを変えては学習を繰り返して、検証データに対して評価指標が最も良くなるようにハイパーパラメータの調整を繰り返していくことになります(交差検証法に対して、ホールドアウト法と呼ばれる)。
これは交差検証に比べれば随分速く1回の評価を終えることができますが、評価としては極めていい加減と言えます。検証データはパラメータの更新自体には使われなくとも、ニューラルネットワークの学習の決め手には明らかに利用されています。人間自体がその検証データに対して上手く行くように調整しすぎている可能性があるのです。
したがって、このようなホールドアウト法では検証データとは別に、更に別のテスト専用のデータを準備しておいて、そのデータで最終的な評価を行うのがベスト(というか必須?)です。
(学習に用いるデータを「training set」、ハイパーパラメータの調整に使うデータを「validation set」、評価のためだけに使うデータを「test set」と呼びます)
最後に
交差検証以外にもブートストラップ法などもあります。
まずは可能であれば最も基本的な交差検証をしっかり使うことが重要だと思われます。
交差検証の分割数は、基本的に大きい方が正確な検証になります。以下では数値的な実験が行われています。
交差検証と汎化誤差
上記の交差検証に関する話題として、以下の書籍に載っている議論を思い出しました。
まずサンプルをK個に分割したとします。ここではk番目のサンプルを学習に参加させないとしましょう。すなわち、この時の事後分布におけるにはk番目のサンプルは含まれていないということです。
このような事後分布による、関数の期待値は と書くことにします(確率変数がであり、番目のサンプルを含んでいないことを明示するためにこう書いている)。
ここで、k番目のサンプルを除いて訓練した事後分布を使った予測分布を考えればそれは、
と書き表されます。この右辺というのは、具体的にはベイズの時によく話している
というものだと思ってください。ベイズでの予測というのはいつでも考えうる全てのを反映させるべく、事後分布で重みづけて使うことになります(事後分布を求めるのが学習・訓練)。
ともかく、このようにして作られた予測分布を使って、訓練に使わなかったk番目のサンプルに対する予測というのができます。
を計算するということでありますが、これに対する対数損失は
となります(負の対数を取っているだけ)。このような対数損失の計算が、全ての分割パターン(今回はK個の分割パターン)について計算することができるはずです。したがって、そのK個の損失全てを平均したものをクロスバリデーション損失関数として
と書き表すことができます。クロスバリデーションの分割数がデータ数に等しくなっていれば、単に分割数として
と書けます。一方で、互いに独立に発生する訓練データ個を使って、未知データを予測する際の対数損失の期待値は
と書け、これは機械学習においてぜひとも最小化したいと考えられる「(データ個で学習した時の)汎化損失」です。これらを見比べましょう。
汎化損失の方は、個のデータを使って推測したによって未知データを予測する際の対数損失の期待値です。
クロスバリデーション損失の方は番目のデータを省いた個のデータを使って推測したを使って未知データを予測する際の対数損失の平均値です。
クロスバリデーション損失における未知データはあくまで手元にあるデータから訓練に使わず省いたものであり、汎化損失での未知データは学習に使われていないあらゆるデータを想定しています。したがって汎化損失とクロスバリデーション損失はそれぞれ完全に同じものではないです。それにしても、その期待値を考えると両者は一致します。
応用上重要なことは、汎化損失というのはその数式的に表現できても、それを手元のデータから直接計算する手立てはありません。一方で、クロスバリデーション損失は知っての通り、計算することができます。
上記の議論について
実際にクロスバリデーションを行う際に、サンプル数と分割数を一致させるのは時間的な都合上かなり難しいでしょう。また評価指標としても負の対数尤度を使うかは分かりません。更に言えば、式の通り、パラメータに関する期待値を取るということは、予測分布をパラメータに関して周辺化して積分するということですが、これはベイズの立場の予測を行う際に現れるものであり、通常の機械学習ではに対して点推定を行うのでこれはできません。
とは言ってもベイズの立場での議論を一旦やめることにすれば、とりあえずに関する期待値を取ることは諦めても良いですし、評価指標として絶対に負の対数尤度を考えなければならないということでもありません(汎化誤差ってもっと広い範囲を指す)。
ってことで、
くらいに思って、とりあえず出来そうならクロスバリデーション使いましょうという話でした(投げた)。