検証データの割合は何対何にするのか — 振って測ったら、効いていたのは割合ではなく件数だった
手元のデータを訓練と検証に分けるとき、8:2 なのか 7:3 なのか。候補30通りのモデル選択を2,000回くり返して、割合を5%から50%まで振って汎化誤差を測りました。山はたしかにありますが、割合を10倍変えても損は1.2倍しか動かず、割合が同じまま件数を4倍にすると6.5倍動きました。選んだ設定で全件を使い直すと山そのものが消えます。数百件を引けないときは k 分割が平均で勝ちますが、1回の勝負では半々です。
要点
- 検証に回す割合を5%から50%まで振ると、選んだモデルをそのまま使う場合の谷は25〜30%にあります。
- 検証を5%に絞ると、超過誤差は谷の1.5〜1.8倍になります。
- 選んだ設定で全件を使って再学習すると谷は消え、50%まで割合が大きいほど良くなります。
- 選抜の質を決めるのは割合ではなく検証の件数で、同じ5%でも損は6.5倍違いました。
- k 分割は N=60 で超過を3割減らしますが、効き幅はデータが増えるほど消えます。
「8:2」の8と2は、何を決めている数字なのか
手元にラベル付きのデータが1,000件あります。モデルの設定——層の数、多項式の次数、罰則の強さ——をいくつか試して、一番良いものを選びたい。そのために何件を選抜用に取り分けるか。8:2、7:3、9:1。どれもよく見ます。
では、その割合は何を良くしているのでしょうか。検証に回した分は学習に使えません。逆に検証が小さすぎれば、選抜そのものが当てにならない。両側から挟まれていることは分かります。分からないのは、挟まれた谷底がどこにあって、そこから外れるとどれだけ損をするのかです。
この記事はその1点だけを測ります。結論を先に書きます。谷底はあります。ただし、割合という単位が間違っていました。
測り方
真の関数を1つ決めて、そこから雑音つきの観測を作ります。
は から の一様分布から引き、雑音は です。
候補は30通りです。多項式の次数 と罰則 の組み合わせを、リッジ回帰で当てはめます。真の関数は多項式ではないので、次数は「上げれば良い」も「下げれば良い」も成り立ちません。どこかに当たりがあり、それを検証データで当てにいく——この記事が測るのはその当て方です。
成績は汎化誤差で見ます。乱数のテスト集合を引くとテスト側の揺れが混ざるので、 の全体にわたる誤差を数値積分(400点のガウス・ルジャンドル求積)で出し、雑音の分 を足しました。照合として、同じ係数を乱数10万件のテスト集合で測ると 0.3045、求積では 0.3046 でした。
この作り方だと、どんなに上手くやっても 0.3000 より下には行けません。雑音そのものの大きさが床です。以下の表では、この床からの超過分(RMSE − 0.3000)も併記します。そこが実質的な「腕の差」になります。
1つの条件につきデータの引き直しを 2,000回くり返し、平均を取りました。乱数の種は固定です。
割合を 5% から 50% まで振る
全部で 件。検証に回す割合 を振ります。選んだモデルはそのまま使う(訓練側だけで学習したものを本番に出す)やり方です。
| 割合 | 検証 | 訓練 | 汎化RMSE | 超過 |
|---|---|---|---|---|
| 5% | 10 | 190 | 0.3152 | 0.0152 |
| 10% | 20 | 180 | 0.3112 | 0.0112 |
| 15% | 30 | 170 | 0.3097 | 0.0097 |
| 20% | 40 | 160 | 0.3087 | 0.0087 |
| 25% | 50 | 150 | 0.3085 | 0.0085 |
| 30% | 60 | 140 | 0.3085 | 0.0085 |
| 40% | 80 | 120 | 0.3090 | 0.0090 |
| 50% | 100 | 100 | 0.3101 | 0.0101 |
谷は 25〜30% です。平均の標準誤差は 0.0001〜0.0005 なので、この差は読んでよい大きさです。
読み方を2つに分けます。
両端は悪い。 5% では超過が 0.0152 で、谷の 1.79 倍。50% では 0.0101 で 1.19 倍。悪くなる理由は左右で別です。左は検証が10件しかないので選抜を誤る。右は訓練が100件に減ってモデル自体が弱くなる。同じ曲線の両側ですが、起きている事故は別物です。
真ん中は平らです。 15%から40%の間は 0.0085〜0.0097——谷から最大14%しか違いません。他の条件( と )でも同じ形でした。
| 全部で | 5%の超過 | 谷の超過 | 50%の超過 | 5%は谷の |
|---|---|---|---|---|
| 60件 | 0.0482 | 0.0318 (25%) | 0.0355 | 1.52倍 |
| 200件 | 0.0152 | 0.0085 (25%) | 0.0101 | 1.79倍 |
| 1,000件 | 0.0027 | 0.0017 (25%) | 0.0021 | 1.59倍 |
検証を5%に絞ると超過が1.5〜1.8倍になるのは が3桁変わっても変わりません。一方、谷の位置は3つとも 25% 付近で動きませんでした。
外すのは普通。問題は外したときの損
「選抜を誤る」をそのまま数えます。同じ訓練データで当てはめた30個のうち、汎化誤差が本当に最小だったもの(事後的にしか分からない)を選べた回の割合です。
では、検証10件のとき 30個の中から当てられたのは 10.1% だけ。検証100件に増やしても 30.4% です。つまり7割の回は「本当の最良」を外しています。
それでも上の表の超過が 0.01 程度で収まるのは、外しても損が小さいからです。選んだものと本当の最良の汎化誤差の差(以下「選択の損」)を測ります。
| 検証 | 選択の損(平均) | 90%点 | 99%点 |
|---|---|---|---|
| 10 | 0.0108 | 0.0460 | 0.0673 |
| 20 | 0.0064 | 0.0091 | 0.0602 |
| 50 | 0.0028 | 0.0051 | 0.0391 |
| 100 | 0.0020 | 0.0048 | 0.0176 |
候補30個の多くは実質同じモデルです。実際に数えると、最良から 0.001 以内に平均 5.1 個、0.005 以内に 13.8 個が入っていました(・訓練160件・1,000回)。次数7で が か かは、ほとんど同じ関数になります。だから「外した」こと自体は事故ではありません。見るのは裾です。検証10件では100回に1回 0.0673 の損——床からの超過が一気に5倍になる回が混ざります。検証100件ではその99%点が 0.0176 まで縮みます。平均より先に裾が直るのが、件数を増やしたときの効き方でした。
選んだ設定で全件を使い直すと、谷が消える
ここまでは「訓練側だけで学習したモデルをそのまま出す」やり方でした。実務でよくやるもう一方——選んだ設定で、訓練と検証を合わせた全件を使って学習し直す——に替えます。同じ実験の同じ条件です。
| 割合 | そのまま使う | 全件で再学習 |
|---|---|---|
| 5% | 0.3152 | 0.3156 |
| 10% | 0.3112 | 0.3114 |
| 20% | 0.3087 | 0.3084 |
| 30% | 0.3085 | 0.3075 |
| 40% | 0.3090 | 0.3072 |
| 50% | 0.3101 | 0.3072 |
山が消えて、右に行くほど良くなります。 50% まで単調です(30%以降は 0.3075→0.3072 でほぼ平ら)。 でも でも、再学習したときの最小は3つとも 50% でした。
理屈は一度言えば終わりです。再学習すれば訓練が減る損は返ってくるので、割合が決めているのは選抜の精度だけになります。そして選抜の精度は検証を増やすほど上がる。谷を作っていたのは「検証に回した分だけ本体が痩せる」という片側だけの事情で、使い直せばその片側が消えます。
つまり「8:2 か 7:3 か」は、選んだ設定で学習し直すかどうかを決めてからでないと答えが決まらない問いでした。
割合を固定して全体を動かすと、何も起きない
ここが本題です。割合という単位が怪しいので、検証の件数を固定したまま全体 を動かす実験をします。測るのは選択の損です。
| 検証 | 全部で | 割合 | 選択の損 |
|---|---|---|---|
| 25 | 200 | 12.5% | 0.0053 |
| 25 | 500 | 5.0% | 0.0052 |
| 25 | 1,000 | 2.5% | 0.0046 |
| 25 | 2,000 | 1.2% | 0.0044 |
| 100 | 200 | 50.0% | 0.0019 |
| 100 | 500 | 20.0% | 0.0011 |
| 100 | 1,000 | 10.0% | 0.0011 |
| 100 | 2,000 | 5.0% | 0.0008 |
上の4行は、割合が 12.5% から 1.2% まで10倍変わっているのに、損は 0.0053 から 0.0044。1.2倍しか動きません。
逆向きに読みます。表の中で割合が同じ 5% の行は2つあります。検証25件()が 0.0052、検証100件()が 0.0008。同じ5%なのに損は6.5倍違います。
全体を固定して件数だけ動かしても同じ形です()。
| 検証 | 割合 | 選択の損 | 損が0.01を超えた率 |
|---|---|---|---|
| 25 | 2.5% | 0.0040 | 5.1% |
| 50 | 5.0% | 0.0020 | 1.7% |
| 100 | 10.0% | 0.0011 | 0.2% |
| 200 | 20.0% | 0.0006 | 0.0% |
| 500 | 50.0% | 0.0004 | 0.0% |
選抜の質を決めているのは件数です。割合は、件数を決めたあとに結果として出てくる数字でした。 「何対何にするか」と聞くと、 が10倍違う2つの現場に同じ答えを出してしまいます。上の表の通り、それは6.5倍外します。
数百件を引けないとき — k 分割
では件数で決めるとして、全体が小さければ引けません。 で検証200件は取れない。ここで出てくるのが k 分割です。データを 個に分け、1つを検証・残りを学習にして全候補を測り、検証に回すかたまりを 回入れ替えて合計し、勝った設定で全件を学習し直します。検証に「回す」のではなく「使い回す」ので、全件が選抜にも学習にも効きます。
固定分割(8:2・選んだ設定で全件再学習)と比べます。同じデータの引きで両方やって対にした比較です(引きの良し悪しを差から外せます)。3,000回の平均。
| 全部で | 固定8:2 | k=5 | 差 | 標準誤差 |
|---|---|---|---|---|
| 60件 | 0.3336 | 0.3235 | 0.0101 | 0.0008 |
| 200件 | 0.3087 | 0.3065 | 0.0022 | 0.0002 |
| 1,000件 | 0.3016 | 0.3011 | 0.0004 | 0.0000 |
では超過(床 0.3000 からの差)が 0.0336 から 0.0235 へ——3割減。 で 0.0087→0.0065、 では 0.0016→0.0011 で、ほぼ無視できる差になります。k 分割が効くのはデータが小さいときだけで、効き幅は とともに消えていきます。 も同じ表で測りましたが(0.3242 / 0.3065 / 0.3011)、 との差を同じ引きで対にすると で 0.0003(標準誤差 0.0003)、 と では 0.0000 ——見えませんでした。
ただし、勝ち方には注意が要ります。1回ごとの勝負で k 分割が勝った割合は 57.1% / 51.9% / 58.5% です。平均では明確に勝つのに、1回では半々。差を作っているのは固定分割がときどき大きく外す回で、ふだんは互角です。「k 分割にしたら良くなった」を1回の実験で確かめようとすると、半分の確率で逆の結論が出ます。
代償は計算です。 分割は候補ごとに 回当てはめるので、素朴にやれば時間は 倍になります。
決め方
測った範囲から言えることだけを書きます。
- 先に決めるのは、選んだ設定で全件を使い直すかどうか。 使い直すなら、割合は大きめ(30〜50%)で損をしません。そのまま出すなら 25% 付近が谷です。
- 次に決めるのは検証の件数です。割合ではありません。この実験では 100件で選択の損が 0.0011、損が 0.0100 を超える回が 0.2% まで落ちました。候補の数と候補どうしの近さで必要量は変わるので、自分の候補集合で同じ表を作るのがいちばん速い確かめ方です。
- その件数を引いても訓練が十分残るなら、割合は気にしなくてよい。 15〜40% の間は谷と14%しか違いませんでした。
- 引けないなら k 分割。 で超過が3割減りました。計算は 倍、そして1回の実験では確かめられません。
再現用の核だけ置きます。
import numpy as np
# 候補30通り: 次数1..10 × 罰則3通り
CANDS = [(d, l) for d in range(1, 11)
for l in (1e-6, 1e-4, 1e-2)]
# リッジ回帰(多項式基底)
def fit(x, y, d, lam):
X = np.vander(x, d + 1, increasing=True)
A = X.T @ X + lam * np.eye(d + 1) * len(x)
return np.linalg.solve(A, X.T @ y)
# 末尾 nv 件を検証に回して1つ選ぶ
def pick(x, y, nv):
xtr, ytr = x[:-nv], y[:-nv]
xv, yv = x[-nv:], y[-nv:]
err = []
for d, l in CANDS:
w = fit(xtr, ytr, d, l)
V = np.vander(xv, d + 1, increasing=True)
r = V @ w - yv
err.append(np.sqrt(np.mean(r * r)))
# 選んだ設定で、全件を使って学習し直す
return CANDS[int(np.argmin(err))]
現場ではこう使う
誰が、いつ決めるか。 表形式のデータで回帰や分類のモデルを作るデータサイエンティストや機械学習エンジニアが、プロジェクトの最初に「評価の段取り」を組むときです。ここで決めた分け方は、その後のすべての比較実験の物差しになります。途中で変えると、それまでの数字と比べられなくなります。
実際に触るもの。
- scikit-learn の
train_test_split(test_size=...)は、test_sizeに小数を渡すと割合、整数を渡すと件数です。この記事の結論に沿うなら、件数で渡す方が意図がそのまま残ります。 GridSearchCV/RandomizedSearchCVは既定でrefit=True、つまり勝った設定で渡したデータ全体を学習し直します。上の「全件で再学習」の側に最初から立っていることになります。KFold(n_splits=5)は既定でshuffle=Falseです。データが日付順やラベル順に並んだままだと、かたまりの中身が偏ります。クラス比を揃えるならStratifiedKFold、同じ人・同じ店舗を片側に寄せるならGroupKFold、時間で切るならTimeSeriesSplitに替えます。- Keras の
model.fit(validation_split=0.2)は、シャッフルする前の末尾2割を検証に取ります。並び順のまま渡すと、検証が「最後の時期だけ」や「最後のクラスだけ」になります。
知らないと事故になる落とし穴。
- 分ける前に前処理を全件で当てる。 標準化や欠損埋めの統計を全件で計算してから分けると、検証の情報が訓練に混ざり、選抜が甘く見えます。前処理は
Pipelineに入れて、分割の内側で当てます。 best_score_をそのまま実力として報告する。 これは選ぶのに使い切った数字です(どれだけ楽観が乗るかは、次の節で触れる別の問題です)。報告には、選抜に一度も触れていない集合を別に残します。- k 分割と固定分割の優劣を1回の実験で決める。 上の表の通り、1回の勝負は半々でした。比べるなら同じ引きで対にして、何回か繰り返します。
面接や設計レビューで聞かれる問いと、答えの筋道。
- 「検証は8:2と7:3のどちらにしますか」→ まず、選んだ設定で全件を学習し直すかを確認する。次に、割合ではなく検証の件数で考え、その件数を引いても訓練が十分残るかを見る。残らないなら k 分割。
- 「k 分割の k はいくつにしますか」→ 倍の計算と引き換えに、データが小さいときほど効く。この記事の範囲では と の差は見えなかった。データが大きければ固定分割との差もほぼ消える。
- 「交差検証のスコアと本番の成績がずれるのはなぜですか」→ 選抜に使った数字の楽観、分け方の中身(時間・グループ)と本番の食い違い、前処理のリーク、の順に疑う。
この記事で答えていないこと
3つあります。
検証で選んだ数字を、そのまま実力として報告していいのか。 これは別の問題です。この記事は「選んだモデルがどれだけ良いか」を、選抜に一度も使っていない物差し(数値積分した汎化誤差)で測りました。現場でその物差しはありません。手元にあるのは検証データの数字だけで、それは選ぶのに使い切った数字です。どれだけ楽観が乗るかは、教科書の第III部 第2章が同じ形の実験で測っています——候補90通りを検証10件で選ぶと、勝者の検証の数字は実力より 0.34 良く見えていました。第2章「手元に合わせすぎる ── 過学習」が続きです。そこまで読むと、この記事の表が「なぜ3つに分けるのか」の答えの半分でしかないことも分かります。
層化と時系列。 この実験は を独立に引いているので、分け方の中身(クラス比を揃える、時間で切る、人や店舗の単位で寄せる)は一切効きません。実データではそこが先に効きます。分け方を誤ると何が混ざるかはデータリークと実験管理で5つの型に分けています。
候補の数。 30通りに固定しました。候補を増やせば選抜はもっと難しくなり、必要な検証件数も増えます。この記事の数字をそのまま持ち出せるのは、候補が数十通りの範囲までです。候補を数百・数千と回す探索のやり方はハイパーパラメータ探索が扱っています。
コメント
コメントにはログインが必要です