0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

機械学習を学び始めると、過学習データリークがごちゃごちゃになりやすいです。

どちらも「学習時はうまくいっているように見えるのに、本番では外れる」という意味では似ています。
でも、原因はまったく違います

この記事では、ダミーデータを使って

  • 過学習とは何か
  • データリークとは何か
  • なぜ「CVが良すぎる」と危ないのか
  • 何を疑えばよいのか

を、初心者向けに順を追って説明します。
コードは Google Colab でそのまま実行できます。

まず結論

最初に一言で言うと、違いは次の通りです。

過学習: モデルが複雑すぎて、訓練データのノイズまで覚えてしまう
データリーク: 本来は使ってはいけない情報が、学習や評価に混ざってしまう

似ているようで、本質はかなり違います。

用語説明

訓練データ(train data)

モデルを学習させるためのデータです。
モデルはこのデータを見て、予測のルールを作ります。

テストデータ(test data)

学習には使わず、最後に「本当に未知データでどれくらい当たるか」を確認するためのデータです。

交差検証(Cross-Validation, CV)

データを何回かに分けて、

  • 一部で学習
  • 残りで評価

を繰り返し、平均的な性能を見る方法です。
「たまたまこの分割だけ良かった」を減らすためによく使います。

過学習(overfitting)

訓練データに合わせすぎることです。
本来学ぶべきパターンだけでなく、たまたま入っているノイズや偶然まで覚えてしまいます。

その結果、

  • train はすごく良い
  • でも test や CV はそこまで良くない

という状態になりやすいです。

データリーク(data leakage)

本来、予測時には使えない情報が、学習や評価に紛れ込むことです。

たとえば、

  • 予測したい未来の情報を特徴量に入れてしまう
  • 正解ラベルにほぼ等しい列が混ざる
  • 前処理をデータ分割前にまとめてやってしまう
  • 同じ人・同じ製品・同じ時系列の未来が train と test の両方に入る

などが典型例です。

その結果、

  • train だけでなく
  • CV や test まで異様に良く見える

ことがあります。

過学習とデータリークの違いを表にすると

観点 過学習 データリーク
原因 モデルが複雑すぎる 使ってはいけない情報が混ざる
何を覚えているか ノイズや偶然 禁止情報・未来情報・答えに近い情報
trainスコア 高くなりやすい 高くなりやすい
CV/testスコア trainより低くなりやすい 不自然に高くなることがある
本番での挙動 徐々に悪くなることが多い 急に崩れることがある
対策 モデルを簡単にする、正則化、データを増やす 分割方法と前処理を見直す、禁止情報を除く

まずはライブラリを読み込む

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

実験1: 過学習とは何かをダミーデータで見る

まずは過学習の例を見ます。

どういうダミーデータを作るか

今回は、分類しやすい2次元データに対して、さらに意味のないノイズ特徴量を40個追加します。
そこに、深い決定木を当てます。

すると、

  • 浅い木は大事なパターンだけを拾う
  • 深い木はノイズまで覚えてしまう

という構図が見えやすくなります。

seed = 42
rng = np.random.default_rng(seed)

# もともとの信号データ(2次元)
X_signal, y = make_moons(n_samples=180, noise=0.45, random_state=seed)

# 意味のないノイズ特徴量を40個追加
noise_features = rng.normal(size=(180, 40))
X = np.hstack([X_signal, noise_features])

# train / test に分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.4, stratify=y, random_state=seed
)

# CVの設定
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=seed)

# 木の深さを変えながら、train / CV / test の精度を比較
records = []

for depth in range(1, 13):
    model = DecisionTreeClassifier(max_depth=depth, random_state=seed)
    model.fit(X_train, y_train)

    train_acc = model.score(X_train, y_train)
    cv_acc = cross_val_score(model, X_train, y_train, cv=cv).mean()
    test_acc = model.score(X_test, y_test)

    records.append([depth, train_acc, cv_acc, test_acc])

result_overfit = pd.DataFrame(
    records, columns=["max_depth", "train_acc", "cv_acc", "test_acc"]
)

best_row = result_overfit.loc[result_overfit["cv_acc"].idxmax()]

print(result_overfit.round(3))
print("\nBest by CV")
print(best_row.round(3))

image.png

だいたい次のような傾向になります。

  • max_depth=2 あたりで CV が最大
  • 深くするほど train は上がる
  • でも CV と test はむしろ下がる
  • max_depth=12 では train=1.000 なのに cv=0.731, test=0.722

続いて図にします。

plt.figure(figsize=(8, 4.8))
plt.plot(result_overfit["max_depth"], result_overfit["train_acc"], marker="o", label="Train accuracy")
plt.plot(result_overfit["max_depth"], result_overfit["cv_acc"], marker="o", label="CV accuracy")
plt.plot(result_overfit["max_depth"], result_overfit["test_acc"], marker="o", label="Test accuracy")

best_depth = int(best_row["max_depth"])
plt.axvline(best_depth, linestyle="--", label=f"Best CV depth={best_depth}")

plt.xlabel("max_depth")
plt.ylabel("Accuracy")
plt.title("Overfitting example: deeper tree fits training data too well")
plt.ylim(0.65, 1.05)
plt.grid(alpha=0.3)
plt.legend()
plt.tight_layout()
plt.savefig("figure1_overfitting_curve.png", dpi=150, bbox_inches="tight")
plt.show()

figure1_overfitting_curve.png

この図の見方はとても重要です。

  • 青線(train) は木を深くするほど上がっていき、最後は 1.0
  • 橙線(CV) は途中で頭打ちになり、むしろ下がる
  • 緑線(test) も同じように下がる

つまり、

訓練データにはどんどん合っていくのに、未知データには強くならない

ということです。

これが過学習です。

過学習は「覚えすぎ」

今回の深い決定木は、データの本質だけでなく、
「このサンプルだけたまたまこうだった」というノイズまで拾ってしまっています。

だから train だけを見ると優秀に見えますが、
CV や test では通用しません。


実験2: データリークとは何かをダミーデータで見る

次はデータリークです。

過学習との違いをはっきりさせるため、今度はモデル自体は単純なロジスティック回帰を使います。
つまり、モデルをやたら複雑にはしません。

それでも、使ってはいけない情報を混ぜると、CVが異様に高く見えてしまいます。

どういうリークを作るか

今回は説明用に、かなり分かりやすいリークを作ります。

  • まず、普通の特徴量 x1, x2, x3 から目的変数 y を作る
  • そのあとで、future_info = y + 小さなノイズ という列を追加する

この future_info は、ほぼ答えそのものです。
当然、本番予測の時点では使えません。

実務ではここまで露骨ではなくても、

  • 未来にしか分からない値
  • 予測後に確定する値
  • 正解ラベルから作った列
  • train/test分割前にやってしまった target encoding

などは、実質的に同じ問題を起こします。

def make_base_data(n, seed):
    rng = np.random.default_rng(seed)
    x1 = rng.normal(0, 1, n)
    x2 = rng.normal(0, 1, n)
    x3 = rng.normal(0, 1, n)

    # 3つの特徴量から y を作る
    logit = 1.2 * x1 - 1.0 * x2 + 0.6 * x3 + rng.normal(0, 1.0, n)
    y = (logit > 0).astype(int)

    X = np.column_stack([x1, x2, x3])
    return X, y


n_train = 400
n_prod = 400

# 学習用データ
X_train_base, y_train = make_base_data(n_train, seed=1)

# 本番を想定した未知データ
X_prod_base, y_prod = make_base_data(n_prod, seed=2)

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

まずはクリーンな特徴量だけで学習する

clean_model = Pipeline([
    ("scaler", StandardScaler()),
    ("lr", LogisticRegression(max_iter=1000))
])

clean_model.fit(X_train_base, y_train)

clean_train = clean_model.score(X_train_base, y_train)
clean_cv = cross_val_score(clean_model, X_train_base, y_train, cv=cv).mean()
clean_prod = clean_model.score(X_prod_base, y_prod)

print("Clean features")
print(f"train_acc = {clean_train:.3f}")
print(f"cv_acc    = {clean_cv:.3f}")
print(f"prod_acc  = {clean_prod:.3f}")

出力は次のようになります。

  • train_acc = 0.840
  • cv_acc = 0.827
  • prod_acc = 0.842

かなり自然です。
train, CV, 本番想定の精度がだいたい近いので、そこまで違和感はありません。

次にリーク列を混ぜる

# ほぼ答えそのもの、という禁止情報を追加
rng_train_leak = np.random.default_rng(10)
future_info_train = y_train + rng_train_leak.normal(0, 0.05, n_train)

X_train_leak = np.column_stack([X_train_base, future_info_train])

leak_model = Pipeline([
    ("scaler", StandardScaler()),
    ("lr", LogisticRegression(max_iter=1000))
])

leak_model.fit(X_train_leak, y_train)

leak_train = leak_model.score(X_train_leak, y_train)
leak_cv = cross_val_score(leak_model, X_train_leak, y_train, cv=cv).mean()

print("With leakage")
print(f"train_acc = {leak_train:.3f}")
print(f"cv_acc    = {leak_cv:.3f}")

次の結果になります。

  • train_acc = 1.000
  • cv_acc = 1.000

モデルは単純なロジスティック回帰なのに、
いきなり完璧です。

この時点で、

これは本当に賢いモデルなのか?
それとも、見てはいけないものを見ていないか?

と疑うべきです。

「リークしたテスト」だとテストも完璧に見えてしまう

参考までに、もしテスト側にも同じように future_info を作ってしまうと、
テスト精度まで完璧に見えます。

rng_test_naive = np.random.default_rng(11)
future_info_test_naive = y_prod + rng_test_naive.normal(0, 0.05, n_prod)

X_test_leak_naive = np.column_stack([X_prod_base, future_info_test_naive])
leak_naive_test = leak_model.score(X_test_leak_naive, y_prod)

print(f"Naive leaked test score = {leak_naive_test:.3f}")

実行すると 1.000 となります。

つまり、リークがデータセット全体に入り込んでいると、CVもtestも両方だまされる ことがあります。

本番を想定するとどうなるか

実際の本番では future_info は存在しません。
そこで、予測時には意味のない値しか入らない状況を、ダミーで再現します。

rng_prod = np.random.default_rng(999)
future_info_prod_like = rng_prod.normal(0, 1, n_prod)  # 本番では意味を持たない値

X_prod_like = np.column_stack([X_prod_base, future_info_prod_like])
leak_prod_like = leak_model.score(X_prod_like, y_prod)

print(f"Production-like score = {leak_prod_like:.3f}")

実行すると約 0.548 まで落ちます。

つまり、

  • train は完璧
  • CV も完璧
  • naive test も完璧
  • でも本番想定では崩壊

です。

これがデータリークの怖さです。

図で比較する

summary = pd.DataFrame({
    "model": ["Clean features", "With leakage"],
    "train_acc": [clean_train, leak_train],
    "cv_acc": [clean_cv, leak_cv],
    "production_like_acc": [clean_prod, leak_prod_like],
}).round(3)

print(summary)

metric_labels = ["Train", "CV", "Production-like"]
clean_scores = [clean_train, clean_cv, clean_prod]
leak_scores = [leak_train, leak_cv, leak_prod_like]

x = np.arange(len(metric_labels))
width = 0.35

plt.figure(figsize=(8, 4.8))
plt.bar(x - width / 2, clean_scores, width, label="Clean features")
plt.bar(x + width / 2, leak_scores, width, label="With leakage")

plt.xticks(x, metric_labels)
plt.ylim(0.4, 1.05)
plt.ylabel("Accuracy")
plt.title("Leakage example: CV can look perfect while production fails")
plt.grid(axis="y", alpha=0.3)
plt.legend()
plt.tight_layout()
plt.savefig("figure2_leakage_bar.png", dpi=150, bbox_inches="tight")
plt.show()

figure2_leakage_bar.png

この図では、

  • Clean features は train / CV / Production-like がほぼ同じ
  • With leakage は train と CV が 1.0
  • でも Production-like0.548

になっています。

ここが、過学習との決定的な違いです。


過学習とデータリークの違いを今回の実験で言い直すと

過学習

  • モデルが深すぎる
  • train には合わせられる
  • でも CV や test は伸びない
  • trainだけ異様に高い

データリーク

  • モデルは単純でも起こる
  • CV まで異様に高くなる
  • test すら高く見えることがある
  • 本当にクリーンな本番では崩れる
  • CVまで異様に高い

つまり、

過学習は「覚えすぎ」
データリークは「見てはいけないものを見ている」

と考えると区別しやすいです。


CVが良すぎるときに疑うべきこと

「CVが高い」こと自体は悪くありません。
でも、高すぎる ときは要注意です。

たとえば、

  • ノイズが多いはずの問題なのに 0.99 や 1.00 に近い
  • シンプルなモデルなのにほぼ完璧
  • 現場感覚ではそんなに簡単な問題ではない
  • test や本番だけ急に悪くなる

といった場合です。

そんなときは次を疑います。

1. 未来情報が混ざっていないか

  • 解約予測なのに「解約手続き開始済み」が入っている
  • 病気予測なのに診断後の検査値が入っている
  • 売上予測なのに集計期間の後半データが入っている

2. 正解ラベルに近い列が入っていないか

  • ラベルそのもの
  • ラベルから作った特徴量
  • 集計のしかた次第で答えが漏れている列

3. 前処理を分割前にまとめてやっていないか

たとえば、

  • 標準化
  • 欠損補完
  • PCA
  • 特徴量選択
  • target encoding

を、train/testに分ける前に全データに対して実行すると危ないことがあります。

今回のコードでは Pipeline を使っているので、
CV の各 fold の内側で正しく前処理されます。

4. 同じ対象が train と test の両方に入っていないか

  • 同じ患者
  • 同じ顧客
  • 同じ製品
  • 同じ画像から切り出したパッチ
  • 同じ時系列の近接時点

こういう場合、モデルは「本質」を学んでいるのではなく、
対象固有のクセを覚えて当てているだけかもしれません。

5. 時系列なのにランダム分割していないか

未来を予測したいのに、
未来側の情報が train に入っていたら、それは本番では再現できません。


よくある誤解

「CVが高ければ安心」

必ずしもそうではありません。
CVの計算のしかた自体が汚れている と、CVも簡単にだまされます。

「過学習とデータリークは同じ」

違います。

  • 過学習は、許された情報を使って学びすぎる
  • データリークは、そもそも使ってはいけない情報を使ってしまう

です。

「正則化すればデータリークも直る」

直りません。
L1/L2 正則化や木の深さ制限は、過学習には効くことがあります。
でも、リークは設計ミスなので、データ分割や特徴量の作り方を直す必要があります。


まとめ

この記事のポイントを最後に整理します。

  • 過学習は、モデルが複雑すぎてノイズまで覚えること
  • データリークは、本来使えない情報が学習や評価に混ざること
  • 過学習では、train は高いが CV/test はそこまで伸びない
  • データリークでは、CV まで異様に高くなることがある
  • 「CVが良すぎる」ときは、まずリークを疑う価値がある

一言でまとめるなら、

過学習は「覚えすぎ」
データリークは「カンニング」

です。

機械学習では、モデルを賢くする前に、
評価のしかたがクリーンかどうか を疑うのがとても大事です。

おわりに

今回は説明のため、かなり分かりやすいリークを人工的に作りました。
実務ではもっと見えにくい形で起こります。

  • 前処理の順番
  • 特徴量作成のタイミング
  • train/test の分け方
  • 同一対象の混入
  • 時系列の扱い

このあたりを丁寧に確認するだけで、
「CVでは神モデルだったのに本番で壊れる」をかなり防げます。

まずは今回のコードをColabで動かして、
過学習では train だけが上がること
データリークでは CV まで不自然に上がること
を自分の目で確認してみるのがおすすめです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?