機械学習では、train-valid-test分割 1 という方法が良く用いられる。

訓練データとテストデータを分けるのは良くわかる(我々は良く「カンニング」と表現するが、これから予測したい対象を使って予測モデルを構築したらうまく予測できるのはほぼ自明である)。しかし、検証データを用いる必要性はちょっとわからないかもしれない。 この分割が何故必要なのか、考えてみようと思う。

イメージを持つ:代表選抜と本番での実力

機械学習の議論をする前に、ちょっとイメージを持っておこう。

********

100m走の選手選抜を考えてみよう。とある高校で、以下のような実力伯仲な5人の100m走者がいるとする。彼らは調子によって \(\pm0.5\) 秒のタイムのぶれがある。

走者 平均タイム
山田 11.20 秒
佐藤 11.21 秒
田中 11.17 秒
橋本 11.13 秒
加藤 11.15 秒

この5名から高校の代表選手を1名決めるために、タイム計測を行うことを考えてみよう。

********

import numpy as np
np.random.seed(0) # 毎回同じ結果が得られるように固定

# 表の情報を記入
name = ["山田", "佐藤", "田中", "橋本", "加藤"]
ave_time = np.array([11.20, 11.21, 11.17, 11.13, 11.15])

# +-0.5秒のぶれの設定(一様分布を仮定)
noise = 1*np.random.random_sample(5) - 0.5 

time = ave_time + noise
time = np.round(time, 3)        # 小数点以下3桁までにする
print("今回のタイム計測の記録")
print(name)
print(time)

その結果、加藤くんが11.074秒で最も早く、代表選手として選ばれた。

さて、翌週に地区大会があったとしよう。ここで加藤くんはどのような結果を出すだろうか? 2

import numpy as np
np.random.seed(0)

ave_time = 11.15
noise = 1*np.random.random_sample() - 0.5
time = ave_time + noise
time = np.round(time, 3)
print("地区大会での加藤くんの記録")
print(time)

これを実行してみると、代表選手として選ばれた時のタイム(11.074秒)に比べて悪いタイム(11.199秒)となってしまった。これは偶然だろうか?「たまたま調子が悪かっただけなのでは?」という気もする。

しかし、地区大会は一発勝負だ。調子が悪かったのかもしれなくとも、2度以上試すことはできない。そこで、100この世界線があり、それぞれの世界線で加藤くんが地区大会で走ったと考えて 3 、そのタイムの平均を取ってみよう。

import numpy as np
np.random.seed(0)

ave_time = 11.15
noise = 1*np.random.random_sample(100) - 0.5
time = ave_time + noise
ave_time = np.mean(time)

ave_time = np.round(ave_time, 3)

print("地区大会での加藤くんの記録の平均")
print(ave_time)

結果は平均11.123秒となり、依然代表選手として選ばれた時のタイム(11.074秒)と比べると遅い。 よくよく考えると、これはアタリマエの話である。100回、1000回と考えれば、加藤くんの記録の平均は11.150秒に収束するはずだ。この11.150秒という記録は、11.074秒に比べれば遅い。

大会に向けての調整をしない、という仮定を置いて考えているので現実世界とは異なる部分もあるが、選考の際に実力を十分に発揮できた人が選抜されやすい、ということが分かったと思う。

機械学習と100m走

なぜこのような100m走の話をしたのだろうか。これは、機械学習のモデル選択と全く同じ状況だからである。

手元に5つの機械学習手法 + ハイパーパラメータの組があるとしよう。 本当は汎化誤差を知ることは不可能であり、全知全能な「天の声」視点に立っていると考えてほしいので、わざとカッコ書きにしている

機械学習手法 + ハイパーパラメータ 汎化誤差 (RMSE)
DecisionTree(max_depth=20) (2.958)
DecisionTree(max_depth=5) (2.921)
RandomForest(n_estimators=100) (2.863)
RandomForest(n_estimators=200) (2.857)
RandomForest(n_estimators=500) (2.854)

それぞれのモデルは、性能評価に用いるデータセットの中身によって、 \(\pm0.2\) のRMSEのぶれが存在するとしよう。

検証データで行いたいのは、この5つのモデルから、汎化誤差が最も良さそうなモデルを1つ選択することである。やってみよう。

import numpy as np
np.random.seed(0) # 毎回同じ結果が得られるように固定

# 表の情報を記入
name = ["DT20", "DT5", "RF100", "RF200", "RF500"]
ave_gen_err = np.array([2.958, 2.921, 2.863, 2.857, 2.854])

# +-0.2のぶれの設定(一様分布を仮定)
noise = 0.4*np.random.random_sample(5) - 0.2 

valid_err = ave_gen_err + noise
valid_err = np.round(valid_err, 3)        # 小数点以下3桁までにする
print("検証データを用いたときのRMSE")
print(name)
print(valid_err)

この結果、RandomForest(n_estimators=500) が RMSE=2.823 で最も良いモデルである、と選択される。

さて、このモデルを本番環境に適用したときの誤差の期待値(=汎化誤差)を知りたい、と考えてみよう。モデル選択を行った際のRMSE値を使えばいいじゃないか、と思うかもしれないが、これは正しい推定値になっているのだろうか?

誤差の期待値とは、先ほどの例における「地区大会における平均タイム」と同じであるから、独立した試行を十分な回数行った場合の平均性能を見てみればよい。

import numpy as np
np.random.seed(0)

ave_gen_err = 2.854
noise = 0.4*np.random.random_sample(100) - 0.2 
test_err = ave_gen_err + noise
ave_test_err = np.mean(test_err)
ave_test_err = np.round(ave_test_err, 3)

print("誤差の期待値(汎化性能の期待値)")
print(ave_test_err)

この結果、 RMSE=2.843 となり、やはりモデル選択を行った際のRMSE (2.823) の方が良い値となっている。

このように、モデル選択を行った際の誤差の値というのは、期待値よりも良くなってしまい、汎化性能を正しく推定できていない。以上のことから、モデル選択を行う検証データセットのみで汎化性能を評価することは良くなく、検証データとは別に、テストデータを準備して、テストデータをもとに汎化性能を評価する必要がある。これが、train-validではなくtrain-valid-test分割を行う必要がある原因である。

  1. 例えば、https://towardsdatascience.com/how-to-split-data-into-three-sets-train-validation-and-test-and-why-e50d22d3e54c に記述がある 

  2. ここでは、簡単のために、「加藤くんは校内選抜と地区大会で同じ能力を発揮する」と仮定している。 

  3. 無茶苦茶な話だが…。