覚えるのと分かるのはどう違うのか — 訓練・検証・テストで過学習を見抜く【ゼロから学ぶAI 第2回】
訓練データでは満点の曲線と、少し外す直線。どちらが良いかは、学習に見せていないデータで測るまで分かりません。前回の5人に、見せていない5人(検証)と最後まで封をしておく5人(テスト)を足して、過学習の曲線、3つに分ける理由、くねくねに罰金をかける正則化までを、すべて手で検算できる数字で追います。動画版つき。
要点
- 学習に使ったデータ(訓練データ)での損失だけでは、新しいデータでどれだけ当たるかは分からない。学習に見せていないデータで測る。
- この記事の例では、訓練で損失0の4次曲線が、見せていない5人では損失5.13。訓練で0.8の直線は0.2だった。
- 多項式の次数を0から4へ上げると、訓練の損失は8.8→0.8→0.57→0.07→0と下がり続けるが、検証の損失は8.8→0.2→0.80→1.23→5.13と、次数1を底に上がる。
- 検証データはモデル選びに使った時点で『見た』データになる。最後の成績は、一度だけ開けるテストデータで測る(この例では直線のテスト損失0.4)。
- 正則化は、損失に『係数の2乗和×λ』の罰を足して、くねくねした曲線を選びにくくする。この例では λ=0.01 で4次曲線の検証損失が5.13から0.52に下がった。
出典: Deep Learning, Chapter 5: Machine Learning Basics · Deep Learning, Chapter 7: Regularization for Deep Learning · Cross-validation: evaluating estimator performance · sklearn.model_selection.train_test_split ほか2件(末尾)
このシリーズについて — 「ゼロから学ぶ AIと技術のしくみ」は、第1回から順に読めば(見れば)積み上がるように作った解説シリーズです。各回に同じ内容の動画版があります。この第2回は第1回(AIは何を「学習」しているのか)の続きです。損失(平均二乗誤差)が分かっていれば読めます。
前回の宿題: 損失0の曲線は良いモデルか
前回は、5人の生徒の勉強時間 と小テストの点数 (説明用の架空のデータ)に、2つのルールを当てはめました。
| 勉強時間 (時間) | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 点数 (点) | 2 | 5 | 5 | 7 | 11 |
- 直線 : 5人での損失(平均二乗誤差)は 0.8
- 4次曲線(5点を全部ぴったり通る曲線): 5人での損失は 0
数字だけ見れば曲線の勝ちです。でも曲線は、0時間勉強した人を −14点、7時間勉強した人を 0点と予想しました。どうも怪しい。では、「怪しい」を数字で言うにはどうすればよいか。それが今回のテーマです。
過去問を丸暗記した生徒
試験勉強にたとえます。過去問を5回分もらった生徒が2人いるとします。
- Aさんは、過去問の答えを丸暗記しました。過去問を解かせれば満点です。
- Bさんは、解き方を身につけました。過去問ではときどき計算を間違えます。
過去問の点数だけを見れば、Aさんのほうが上です。でも本番の試験は、過去問と違う問題が出ます。本当に知りたいのは本番の点数で、それはまだ見ていない問題を解かせてみるまで分かりません。
機械学習でも同じです。学習に使ったデータを訓練データと呼びます。訓練データでの損失は「過去問の点数」にあたります。知りたいのは、まだ見ていないデータでの損失、つまり本番の点数のほうです。
見せていないデータで測る: 検証データ
そこで、新たに5人分のデータを用意し、学習には一切使わずに取っておきます。これを検証データと呼びます(こちらも架空のデータです)。
| 勉強時間 (時間) | 0.5 | 1.5 | 2.5 | 3.5 | 4.5 |
|---|---|---|---|---|---|
| 点数 (点) | 1 | 3 | 6 | 7 | 9 |
前回の5人で作った2つのルールに、この5人の点数を予想させます。
直線 の予想は 1, 3, 5, 7, 9 点。正解との差は 0, 0, 1, 0, 0 です。差の2乗の平均は
4次曲線の予想は次のとおりです。
| 勉強時間 | 0.5 | 1.5 | 2.5 | 3.5 | 4.5 |
|---|---|---|---|---|---|
| 正解 | 1 | 3 | 6 | 7 | 9 |
| 曲線の予想 | −3.55 | 4.38 | 4.95 | 5.63 | 8.95 |
いちばん大きく外したのは0.5時間の人で、正解1点に対して −3.55点。この1人だけで差の2乗は約20.7です。5人の平均を取ると、曲線の検証損失は 5.13 になります。
| 訓練の損失(前回の5人) | 検証の損失(見せていない5人) | |
|---|---|---|
| 直線 | 0.8 | 0.2 |
| 4次曲線 | 0 | 5.13 |
訓練では曲線が勝ち、検証では直線が圧勝しました。曲線は前回の5人の点数を丸暗記していただけで、勉強時間と点数の関係をつかんでいなかったのです。このように、訓練データにだけ合いすぎて、見ていないデータで外すことを過学習と呼びます。
直線の検証損失(0.2)が訓練損失(0.8)より小さいのは、たまたまです。5人ずつしかいないと、どの5人が入ったかという運で数字が大きく動きます。「検証は訓練より必ず悪くなる」わけではありません。大事なのは、学習に見せていないデータで測ることです。
過学習の曲線: 次数を上げると何が起きるか
直線と4次曲線の間にも、ルールの候補はあります。多項式の次数を0から4まで変えて、それぞれ前回の5人(訓練データ)でいちばん損失が小さくなるように当てはめ、訓練と検証の損失を並べます。
- 次数0: 水平な線(全員に同じ点数を予想する。ここでは平均の6点)
- 次数1: 直線(前回の と同じ線になる)
- 次数2: 放物線
- 次数3: 3次曲線
- 次数4: 5点を全部通る4次曲線
| 次数 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 訓練の損失 | 8.8 | 0.8 | 0.57 | 0.07 | 0 |
| 検証の損失 | 8.8 | 0.2 | 0.80 | 1.23 | 5.13 |
2本の線にすると、形がはっきりします。
- 訓練の損失は、次数を上げるほど下がり続けます。 曲げられる自由が増えるほど、手元の5点に寄せられるからです。次数4で0になります。
- 検証の損失は、次数1で底を打ち、そこから上がります。 次数2で0.80、3で1.23、4で5.13。
左側(次数0)は、ルールが単純すぎて、手元のデータにすら合っていない状態です。これを学習不足(アンダーフィッティング)と呼びます。右側(次数3・4)は、訓練の損失はほぼ0なのに検証の損失が上がっていく状態で、これが過学習です。訓練と検証の差が開いていくことが、過学習のいちばん分かりやすいサインです。
この例でいちばん検証損失が小さいのは次数1、つまり直線です。私たちはこの表を見て、直線を選びます。
なぜ3つに分けるのか: テストデータ
ここで一つ、落とし穴があります。私たちは5つの候補の中から、検証の損失がいちばん小さいものを選びました。つまり検証データは、もう「選ぶために見た」データです。
たとえるなら、模試の結果を見ながら勉強法を5通り試し、いちばん模試の点が良かった方法を選んだ状態です。選んだ方法の模試の点は、その模試にたまたま合っていた分だけ、良く見えている可能性があります。候補が多いほど、運良く低い数字が出た候補を拾いやすくなります。
そこで、データを3つに分けます。
| 呼び名 | 何に使うか | たとえ |
|---|---|---|
| 訓練データ | パラメータ(つまみ)の値を決める | 過去問で練習する |
| 検証データ | 候補(次数など)を比べて選ぶ | 模試で勉強法を選ぶ |
| テストデータ | 最後に一度だけ、成績を測る | 本番の試験 |
テストデータは、選び終わるまで封をしておき、最後に一度だけ開けます。このシリーズの例では、さらに5人分をテスト用に取っておきました(架空のデータ)。
| 勉強時間 (時間) | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 点数 (点) | 3 | 4 | 6 | 8 | 9 |
選んだ直線 の予想は 2, 4, 6, 8, 10 点。差は 1, 0, 0, 0, −1 で、テスト損失は
外に向けて報告するのは、この 0.4 です。検証の0.2ではありません。
テストの結果を見てから、また次数を選び直したらどうなるでしょう。そのときテストデータは「選ぶために見た」データになり、検証データと同じ立場に落ちます。テストは何度も開けない、というのが3つに分ける意味です。
実際の分け方は、ここでの5人ずつよりずっと大きく取ります。scikit-learn の
train_test_splitは、割合を指定しないとデータの25%をテスト側に回します。件数が少なくて固定の検証データを取る余裕がないときは、訓練データを何通りかに分けて順番に検証役を回す交差検証を使います(検証データの割合は何対何にするのかで、割合と件数のどちらが効くかを実測しています)。
正則化: くねくねに罰金をかける
過学習を防ぐ方法は、単純なモデルを選ぶことだけではありません。曲がれる自由は残したまま、むやみに曲がるのを嫌がらせる方法もあります。これを正則化と呼びます。
4次曲線を、係数を使って書きます。
5点を全部通る曲線では、係数は 、、、 でした。大きな係数どうしが打ち消し合って、無理やり5点を通っています。そこで、損失に係数の大きさの罰を足します。
- 前半は前回と同じ平均二乗誤差。データに合っているほど小さい
- 後半は罰。係数 から の2乗を足したもの(切片 は罰の対象外)
- (ラムダ)は罰の強さ。0なら罰なし、大きいほど係数を小さく保とうとする
学習は、この合計がいちばん小さくなる係数を探します。データにぴったり合わせようとして係数が大きくなると、罰が増えます。結果として、少しデータから外れても、なだらかな曲線が選ばれやすくなります。この形の罰を使った回帰をリッジ回帰と呼びます。
を変えて、前回の5人で4次曲線を当てはめ直した結果です。
| 0 | 0.001 | 0.01 | 0.1 | 1 | |
|---|---|---|---|---|---|
| 係数の2乗和 | 932 | 24.7 | 4.29 | 1.17 | 0.04 |
| 訓練の損失 | 0 | 0.12 | 0.16 | 0.28 | 0.55 |
| 検証の損失 | 5.13 | 0.65 | 0.52 | 0.76 | 1.39 |
- を上げると、係数の2乗和は932から0.04まで縮みます
- 訓練の損失は0から少しずつ上がります(もう5点を全部は通らない)
- 検証の損失は、5.13から0.52まで下がり、 では1.39へ上がり直します(罰が強すぎて係数が小さく抑えられ、今度はデータに合わなくなる。訓練の損失も0.55まで上がっている)
次数のときと同じ谷の形です。 も、検証の損失で選びます。この例では が選ばれます。
ただし、この例では正則化した4次曲線(検証0.52)より、ただの直線(検証0.2)のほうが良い結果でした。データが直線に近い関係でできているからです。正則化は「どんな場合でも勝つ技」ではなく、複雑なモデルを使うときに、過学習を抑えるつまみだと考えてください。深層学習の教科書(Deep Learning 第7章)も、実際の深層学習では、いちばん良いモデルはたいてい「大きなモデルに適切な正則化をかけたもの」だと書いています。
ここで や次数のように、学習の前に人が決める値をハイパーパラメータと呼びます。前回の学習率もそうです。パラメータ(係数)は訓練データで決め、ハイパーパラメータは検証データで決める。この役割分担を覚えておくと、3つに分ける理由が見えやすくなります。
コードで見る
NumPy の polyfit で、次数0から4までの訓練・検証の損失を出すコードです。
import numpy as np
x_tr = np.array([1, 2, 3, 4, 5]); y_tr = np.array([2, 5, 5, 7, 11])
x_va = np.array([0.5, 1.5, 2.5, 3.5, 4.5]); y_va = np.array([1, 3, 6, 7, 9])
mse = lambda c, x, y: np.mean((np.polyval(c, x) - y) ** 2)
for deg in range(5):
c = np.polyfit(x_tr, y_tr, deg)
print(deg, round(mse(c, x_tr, y_tr), 2), round(mse(c, x_va, y_va), 2))
出力:
0 8.8 8.8
1 0.8 0.2
2 0.57 0.8
3 0.07 1.23
4 0.0 5.13
polyfit は訓練データ(x_tr, y_tr)だけを見て係数を決めます。検証データ(x_va, y_va)は損失を測るときにしか使っていない、という点がポイントです。
現場ではこう使う
データサイエンティストや機械学習エンジニアは、新しい予測課題に取りかかると、まずデータを分けてから手を動かします。分ける前にデータを眺めて前処理を決めたり、全体で特徴量を作ったりすると、検証やテストの情報が学習側に漏れ、数字が良く見えすぎるからです。
実際に触る名前
- scikit-learn の
train_test_split(X, y, test_size=..., random_state=...): 訓練とテストに分ける。test_sizeもtrain_sizeも指定しないと、テストは25% - scikit-learn の
cross_val_score(model, X, y, cv=...): 交差検証。cvを指定しないと5分割 - scikit-learn の
Ridge(alpha=...): この記事のリッジ回帰。ただし公式の目的関数は「二乗誤差の合計+ alpha × 係数の2乗和」なので、この記事の(平均を使った) とは の関係になる - PyTorch のオプティマイザの
weight_decay: 更新のたびに重みを少しずつ0へ引き戻す引数で、係数の2乗和に罰をかけるのと同じ働きをする(torch.optim.SGDの場合)
知らないと事故になる落とし穴
- 分ける前に全体で前処理する(データリーク)。 たとえば全データの平均と標準偏差で標準化してから分けると、検証・テストの情報が訓練に混ざります。標準化の平均と標準偏差は訓練データだけから計算し、それを検証・テストにも当てはめます
- テストを何度も開ける。 テストの数字を見て設定を直すのを繰り返すと、テストは検証と同じ立場になり、報告する数字が楽観的になります
- 時系列データをシャッフルして分ける。 明日の値を予想するモデルを、未来のデータも混ざった訓練データで作ると、本番では手に入らない情報で当てていることになります。時間順に、過去で訓練して未来で検証します(scikit-learn なら
TimeSeriesSplit) - 同じ人・同じ物のデータが訓練と検証の両方に入る。 同じ患者の別の写真が両側にあると、モデルは「その人」を覚えるだけで高い点が出ます。人や物の単位で分けます(
GroupKFold)
面接・実務で問われる問い
- 「訓練の精度は99%、検証は70%。何が起きていて、何をするか」 — 筋道: 訓練と検証の差が大きいので過学習を疑う。モデルを単純にする、正則化を強める、データを増やす、のどれかを、検証の数字を見ながら試す
- 「検証データとテストデータは何が違うのか。検証だけでは駄目なのか」 — 筋道: 検証はモデルやハイパーパラメータを選ぶのに使うので、選んだ時点で楽観的になる。最後の成績は、選ぶのに使っていないテストで一度だけ測る
- 「正則化の強さはどう決めるか」 — 筋道: 候補をいくつか用意し、検証データ(または交差検証)の損失がいちばん小さいものを選ぶ。この記事の の表がその手順そのもの
まとめ
- 訓練データでの損失は「過去問の点数」。知りたいのはまだ見ていないデータでの損失
- データは訓練・検証・テストの3つに分ける。訓練でパラメータを決め、検証で候補を選び、テストは最後に一度だけ開ける
- この例では、次数を上げると訓練の損失は下がり続け、検証の損失は次数1を底に上がった。訓練が下がるのに検証が上がったら、過学習を疑う
- 正則化は損失に「係数の2乗和×」の罰を足し、むやみに曲がるのを抑える。 も検証で選ぶ
次回(第3回): ニューラルネットは何が直線と違うのか — 直線を折り曲げる「活性化」と、層を重ねる意味。
もっと詳しく
- 過学習と評価設計 — 交差検証、データリーク、不均衡データでの指標の選び方
- 検証データの割合は何対何にするのか — 割合を振って測った実験
- 重みの初期化と正則化 — weight decay と dropout をニューラルネットで
コメント
コメントにはログインが必要です