過学習・正則化・交差検証 ― なぜCVするのか、なぜL1/L2が効くのか
公開ノートに必ずあるCVと正則化の意味を詰めます。過学習の正体、バイアス-バリアンス分解の直感、Ridge/Lassoが抑えているもの、正則化がベイズのMAP推定と同じだという視点。KFold/Stratified/Group/TimeSeriesSplitが答える問いと、前処理をCVの外でやるリークを扱います。
by Akisame
なぜ公開ノートは、わざわざデータを分割して何度も学習し直す(交差検証する)のか。そして Ridge や Lasso のように、わざわざペナルティ(正則化項)を足したモデルのほうが、なぜテストで成績が良くなるのか。答えは一行で言えます。汎化性能を訓練データの外で見積もるのがCV、モデルの複雑さにペナルティをかけて過学習を抑えるのが正則化です。
この回では、過学習をバイアス-バリアンスで分解し、L1とL2が何を抑えているのかを式の最小限まで降ろし、最後に「正則化はベイズのMAP推定と同じものを別の言葉で言っているだけ」という見方を渡します。あわせて、課題の構造に応じたCVの選び方と、写経で最も静かに事故る「前処理をCVの外でやるリーク」を実務の時系列データで再現して直します。これは前回(第4回)で作った最適化の土台の、ちょうど真上にある話です。
到達目標
- 過学習を「訓練は良いのに汎化が悪い」状態として、バイアス-バリアンスの言葉で説明できる。
- L1(Lasso)とL2(Ridge)が抑えているものの違い(特にスパース性)を言える。
- 正則化=MAP推定、という対応を理解し、
λを事前分布の強さとして読める。 - KFold / StratifiedKFold / GroupKFold / TimeSeriesSplit が、それぞれどんな問いに答えるCVかを選び分けられる。
- 前処理をCVの外でやる事故と時間リークを、構造的に防ぐ書き方ができる。
前提
第3回で線形回帰(最小二乗)を $y = Xw$ の形で読み、第4回で勾配降下と「損失を最小化する」という発想を作りました。この回はその続きで、「では、その最小化をどこまでやるべきか」「学習の良し悪しをどこで測るべきか」を扱います。pandas での集計と scikit-learn の .fit() / .predict() を写経で回せること、対数変換(第2回)の意図がわかることを前提にします。
過学習の正体(訓練は良いのに汎化が悪い)
過学習とは、モデルが訓練データのたまたまの揺らぎ(ノイズ)まで覚え込んでしまい、未知のデータで成績を落とす状態です。直感的には、点を全部通る曲線をどこまでもくねらせれば、訓練データの誤差はゼロに近づきます。けれどそのくねりは、次に来るデータには対応しません。
これを可視化する標準的な道具が学習曲線・検証曲線です。モデルの複雑さ(ここでは正則化の強さ λ)を動かしながら、訓練スコアと検証スコアを並べてプロットします。典型的には、訓練スコアは複雑にするほど単調に良くなり、検証スコアは途中で最良点を打ってから悪化する「弓なり」を描きます。この弓なりの底が、汎化にとっての最良点です。
公開データのHouse Prices(住宅価格の回帰)で、Ridgeの λ を対数スケールで動かして検証曲線を引いてみます。他者の公開Notebookは貼らず、要点だけを自前で最小再実装します。
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import validation_curve
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# 目的変数は対数化(第2・3回で見た右に裾を引く分布への対処)
y = np.log1p(train["SalePrice"])
X = train[numeric_features].fillna(train[numeric_features].median())
# 正則化の強さ λ を対数スケールで広く動かす
lambdas = np.logspace(-2, 4, 13)
# 標準化 → Ridge を一つのパイプラインに束ねる
# この一体化が、後で扱う「前処理リークの回避」の核になる
model = make_pipeline(StandardScaler(), Ridge())
train_scores, valid_scores = validation_curve(
model, X, y,
param_name="ridge__alpha", # sklearn では正則化強度は alpha と呼ぶ(= λ)
param_range=lambdas,
scoring="neg_root_mean_squared_error",
cv=5,
)
# 訓練スコアは λ を小さくするほど良化、検証スコアは途中で最良→悪化(弓なり)
# 検証スコアが最良になる λ が、過学習と未学習の境目バイアス-バリアンス分解(直感+最小限の式)
過学習・未学習を一つの式で見渡せるのがバイアス-バリアンス分解です。新しい点に対する二乗誤差の期待値は、近似的に次の三つに分かれます。
直感だけ掴めば十分です。バイアスは「モデルが単純すぎて真の関係を取りこぼす分」、バリアンスは「訓練データが変わるたびに予測がブレる分」、最後の項は「どんなモデルでも消せないノイズ」です。複雑にするとバイアスは下がるがバリアンスが上がる。単純にするとその逆。過学習はバリアンスが勝った状態、未学習はバイアスが勝った状態で、検証曲線の弓なりはこの綱引きの可視化に他なりません。正則化は、この綱引きをバリアンスを抑える側へ少し引き戻す操作です。
正則化(Ridge=L2 / Lasso=L1)が抑えているもの
正則化は、最小二乗の目的関数に「重みの大きさへのペナルティ」を足すだけの操作です。Ridge(L2)とLasso(L1)は、ペナルティの測り方が違います。
左の項(あてはまりの良さ)は両者で同じです。違うのは右のペナルティで、Ridgeは重みの二乗和、Lassoは重みの絶対値和にペナルティをかけます。この測り方の差が、係数の縮み方の決定的な違いを生みます。
L2は重みを小さく、L1はゼロにする(スパース)
L2は全部の係数を「小さく」しますが、ちょうどゼロにはしにくい。L1は多くの係数を「ちょうどゼロ」にします。つまりLassoは、回帰を解くのと同時に特徴選択をしているわけです。なぜL1だけがゼロを作るかは、ペナルティの等高線が原点で尖っている(角を持つ)からですが、ここでは挙動だけ確かめれば十分です。
from sklearn.linear_model import Ridge, Lasso
ridge = Ridge(alpha=10.0).fit(X_std, y)
lasso = Lasso(alpha=0.01).fit(X_std, y)
# L2(Ridge): すべての係数を一様に小さくする(ゼロにはなりにくい)
# L1(Lasso): 多くの係数をちょうどゼロにする=特徴選択が同時に起きる
n_zero_ridge = np.sum(np.isclose(ridge.coef_, 0)) # ほぼ 0 本
n_zero_lasso = np.sum(np.isclose(lasso.coef_, 0)) # 多数がゼロになる
# 特徴量の数が標本数より多い状況(`p > n`)では、
# L1 のスパース化が「効く特徴だけ残す」フィルタとして働きやすい正則化=MAP推定、という翻訳(事前分布という見方)
ここがこの回で一番言いたいところです。正則化は新しい発明ではありません。ベイズ推定のMAP(事後確率最大化)と同じものを、別の言葉で言っているだけです。
最尤推定(第4回)は、データの尤もらしさ $\log p(y \mid X, w)$ だけを最大化しました。MAPはそこに、重みそのものへの事前分布 $\log p(w)$ を足して最大化します。
この第二項に、平均ゼロのガウス事前 $p(w) \propto \exp(-\tfrac{\lambda}{2}\|w\|_2^2)$ を入れると、対数を取った瞬間にL2ペナルティ $-\tfrac{\lambda}{2}\|w\|_2^2$ が現れます。つまりRidge=ガウス事前のMAPです。同様に、裾の重いラプラス事前を入れるとL1ペナルティが出てきて、Lasso=ラプラス事前のMAPになります。λ は「事前分布をどれだけ強く信じるか」の度合いで、大きいほど「重みは小さいはずだ」という事前の信念で推定を引き戻します。
交差検証の種類と選び方
検証曲線の最良点を探すにせよ、最終的な性能を見積もるにせよ、汎化性能は訓練データの「外」で測る必要があります。それが交差検証(CV)です。問題は、写経では「とりあえず KFold」になりがちなことです。CVの切り方は課題の構造に合わせて選ばないと、検証スコアそのものが嘘になります。
KFold / StratifiedKFold
KFold は、各行が独立同分布(i.i.d.)と見なせるときの素直な分割です。分類で、クラス比が偏っている(不均衡な)ときは StratifiedKFold を使い、各foldのクラス比を全体と揃えます。揃えないと、foldによって正例がほとんど入らず、評価が不安定になります。
GroupKFold / TimeSeriesSplit
同じユーザの行が複数ある(一人の顧客に複数の注文があるなど)データでランダムに分割すると、同じ人の行が訓練と検証の両方に入り、「その人をすでに見たうえでその人を当てる」ことになります。これを防ぐのが GroupKFold で、人(グループ)単位で分けます。そして時間を含むデータでは、過去で学習して未来で検証する TimeSeriesSplit を使います。本番では常に「未来を予測する」のですから、検証もその順序を守らないと、見積もりが楽観に偏ります。
from sklearn.model_selection import (
KFold, StratifiedKFold, GroupKFold, TimeSeriesSplit, cross_val_score
)
# 課題構造で CV を選ぶ(「とりあえず KFold」をやめる)
# - KFold: 各行が i.i.d. と見なせるとき
# - StratifiedKFold: 分類でクラス比を各 fold に保ちたいとき(不均衡対策)
# - GroupKFold: 同一ユーザの行が複数あり、人単位で分けたいとき
# - TimeSeriesSplit: 時間順を保ち「過去で学習→未来で検証」したいとき
# 前処理を Pipeline に入れると、各 fold の「訓練側だけ」で fit される
# → テスト側の統計量が漏れる事故(リーク)を構造的に防ぐ
pipe = make_pipeline(StandardScaler(), Ridge(alpha=10.0))
scores = cross_val_score(pipe, X, y, cv=KFold(5, shuffle=True, random_state=0))リーク再訪 ― 前処理をCVの外でやる事故、時間リーク
第1回で「コピペが事故る3つの瞬間」の一つに挙げた、前処理をCVの外でやる事故をここで回収します。たとえば全データで平均・分散を計算して標準化してから分割すると、検証foldの情報が前処理を通じて訓練側に漏れます。スコアは良く見えますが、本番では再現しません。防ぎ方は単純で、前処理をモデルと一つの Pipeline に束ね、CVの内側で fold ごとに fit させることです。上のコードで標準化を make_pipeline に入れたのは、まさにこのためでした。
もう一つ、実務で最も多いのが時間リークです。コンペデータは固定でクリーンですが、実務データは時間とともに動きます。ここで過去の受託案件をもとに設計した架空のD2CブランドSOLNAに移します。orders から「翌月の再購入」を予測する設定で、ランダム KFold がどれだけ楽観的なスコアを出すかを再現し、TimeSeriesSplit で直します。
# SOLNA: orders から「翌月の再購入」を予測(時系列を含む設定)
# 特徴量は過去の購入履歴・チャネルなどから作る。order_at を保持しておく。
X = features # order_at を含むデータから作った特徴量
y = will_repurchase # 翌月に再購入したか(0/1)
from sklearn.model_selection import KFold, TimeSeriesSplit, cross_val_score
# 悪い例:ランダム KFold
# 未来の行が訓練に混ざり、過去を「予測」してしまう。
# 検証スコアは高く出るが、本番(常に未来を予測)では再現しない。
naive = cross_val_score(
pipe, X, y, cv=KFold(5, shuffle=True, random_state=0)
)
# 良い例:TimeSeriesSplit
# order_at で並べ、過去→未来の順でだけ検証する。
df = df.sort_values("order_at")
honest = cross_val_score(
pipe, X.loc[df.index], y.loc[df.index], cv=TimeSeriesSplit(5)
)
# naive.mean() は honest.mean() より楽観的に出る。
# その差が、時間リークが水増ししていた分の大きさ。対応書籍と読みどころ
- 門脇ら『Kaggleで勝つデータ分析の技術』― バリデーション設計の章が本回の実装のバックボーンです。CVの切り方が課題と合っていないと、コンペでも実務でも順位・性能の数字そのものが信用できなくなる、という勘所を具体例で確認できます。時系列・グループを含む分割の作法はここで身につけるのが早いです。
PRMLへの一歩
この回は、PRML 1.1(多項式フィッティングと過学習)と 3.1〜3.3(正則化最小二乗・ベイズ線形回帰)の前段です。1.1で見る「次数を上げると訓練誤差は下がるが汎化が崩れる」例は、本回の検証曲線そのものです。そして3.3で「正則化=MAP、その先に事後分布」と展開される筋道を、本回の「正則化=MAP」の節で先取りしました。PRMLを開いたとき、この回が地図のどこにあたるかを思い出せれば十分です。
章末チェックリスト
- 過学習を「訓練は良いのに汎化が悪い」状態として、バイアス-バリアンスの言葉で説明できる
- L1とL2の違い(スパース性)を、ペナルティの測り方の差として言える
- 正則化=MAPの対応を理解し、
λを事前分布の強さとして読める - 課題に合うCV(不均衡=Stratified、グループ=Group、時系列=TimeSeries)を選べる
- 前処理をPipelineでCV内に閉じ、時間リークを再現・修正できる
ここまでが「学習をどこまでやり、どこで測るか」という実装側(HOW)の話でした。では、その検証スコアや予測を自社で何の意思決定に使うのか――何を測り、どこまでが測定の上限かは、対になる記述・予測編で扱っています。次回は、tabularの主役であるGBDT(LightGBM/XGBoost)を、決定木から積み上げて「一行の裏側」まで解剖します。