データリークとは? 特徴選択の順番だけで乱数の精度が100%になった実験
約16分で読めます
Last updated on

データリークとは? 特徴選択の順番だけで乱数の精度が100%になった実験


「交差検証をしているのに、本番では全く当たらないモデル」の話は、機械学習をやっていれば一度は聞いたことがあると思う。原因としてよく挙げられるのがデータリーク——テストデータの情報が、何らかの形で訓練プロセスに漏れてしまうことだ。中でも有名なのが「特徴選択を交差検証の外でやってしまう」パターンで、マイクロアレイの遺伝子発現解析の時代から警告され続けている古典的な落とし穴だ(Ambroise & McLachlan, 2002)。

ただ、私はこの話をいつも「気をつけましょう」という標語としてしか知らなかった。実際にやらかしたとき、精度は何%水増しされるのか。データの次元数や選ぶ特徴の数で、その水増しはどう変わるのか。そして「前処理を全データでやるのもリーク」とよく言われるが、それは特徴選択のリークと同じくらい危険なのか。数字で見たことが一度もなかったので、真の信号が一切ないデータ——つまり純粋な乱数——を使って、幻の精度がどこまで出るのかを実測してみた。

実験に使ったコードの全文はGitHubに置いています。

先に結論:交差検証に渡すのは手順全体

テスト側の答えを使って特徴を選ぶと、その後で何分割しても情報は消えない。 問題集の答えを見て出題範囲を絞り、その範囲で抜き打ち試験を受けるようなものだ。

手順検証用のラベルを特徴選択に使うか評価しているもの
全データで特徴選択 → 交差検証使ってしまう答えの情報が混ざった成績
分割 → 訓練側で特徴選択 → 検証側に適用使わない未知のデータへの予測性能

実験では前者が20シードすべてで100%になった条件がある。後者はおおむね50%。直し方のコードは後半に載せたので、手元の実装を確認したい方は「Pipelineに入れる」の節から読んでもよい。

実験設計: 乱数の特徴と乱数のラベルから「当たる」モデルを作る

材料は2つだけ。n=100サンプル × p次元の標準正規乱数の特徴行列XXと、それとは完全に独立に生成したランダムな2値ラベルyyだ。ラベルは等確率なので、独立な未知データへの精度の期待値は0.5になる。ただし、有限個のテストでは偶然に0.5を超えることもある。そこで、同じデータ・分割で手順を比較し、複数シードで繰り返して、系統的な水増しを確かめた。

このデータに対して、次の3条件で5-fold層化交差検証(分類器はロジスティック回帰)の精度を測った。

  • (A) 特徴選択リーク: 全データを使ってラベルとの相関の絶対値が大きい上位kk個の特徴を選び、その後で交差検証する。標準化はfold内で正しく行う
  • (B) 正しいCV: 特徴選択も標準化も、交差検証の各foldの訓練側のデータだけで行う
  • (C) 標準化リーク: 標準化(StandardScaler)だけを全データで先にfitし、特徴選択はfold内で正しく行う。「前処理を全データでやるリーク」の害の大きさを測る対照実験

条件(A)は、論文やKaggleカーネルで本当によく見る手順だ。「まず相関の高い特徴に絞ってから、交差検証で評価しました」——一見丁寧な仕事に見えるが、特徴を選ぶ段階でテストfoldになるはずのラベルまで見てしまっている。

これを、特徴数ppを100から100,000まで(対数で7段階)、選ぶ特徴数kkを10から1,000まで(5段階)振り、各セルで乱数シードを20個変えて平均した。

結果1: pを増やすほど、幻の精度は上がっていく

まずk=10k=10(上位10特徴を選ぶ)に固定して、特徴数ppを振った結果がこれだ。

横軸に特徴数pを100から100,000までログスケールで取り、縦軸に5-fold交差検証精度を取った折れ線グラフ。オレンジの線(特徴選択を全データでやったリークあり条件)はp=100で精度0.68から始まり、pが増えるにつれ右肩上がりに伸びてp=100,000で0.88に達する。青の線(すべてfold内で行う正しいCV)と緑の破線(標準化だけ全データで行うリーク条件)はほぼ完全に重なり、全域で偶然の水準0.5の点線の周りに張り付いている。各線の周りには20シードの標準偏差の帯が描かれている

データは純粋な乱数なのに、リークあり(オレンジ)はp=100p=100の時点ですでに平均0.676。そしてppを増やすほど幻の精度は上がり続け、p=100,000p=100,000では平均0.883(シードによっては0.93)に達した。一方、同じデータ・同じ分類器で選択をfold内に移しただけの正しいCV(青)は、全域で0.49〜0.52と偶然の水準に張り付いている。

「次元を増やすほど水増しがひどくなる」のは、偶然の相関の最大値がppとともに伸びるからだ。次元の呪いを実測した回では高次元で距離が潰れる話をしたが、今回は同じ高次元が逆に「偶然の当たり」を大量に供給する側に回っている。1つの特徴とラベルの標本相関は、真の相関がゼロでもn=100n=100なら標準偏差約1/n=0.11/\sqrt{n}=0.1で揺らぐ。pp個の候補から最大値を取ると、その値はおよそ2ln⁡p/n\sqrt{2\ln p / n}で成長する——p=100,000p=100,000なら0.48程度の「見かけ上の強い相関」が偶然だけで出てしまう。全データで選択する手順は、この偶然の当たりくじをpp枚の中から探し出してくる作業に他ならない。

結果2: p×kのヒートマップに広がる「精度1.000」の領域

次にppとkkの全組み合わせを見る。ここが今回一番驚いた結果だった。

縦にスタックした2枚のヒートマップ。上の(A)は特徴選択を全データでやってから交差検証したリークあり条件、下の(B)は特徴選択をfold内の訓練データだけでやる正しいCV。どちらも横軸が特徴数p(100から100,000の7段階)、縦軸が選択する特徴数k(10から1,000の5段階)で、各セルに20シード平均の交差検証精度が数字で書かれ、オレンジの濃淡(薄い=0.5付近、濃い=1.0)で塗られている。上のリークあり条件はp=100・k=10の0.68から右上に向かって濃くなり、p≧10,000かつk≧316の領域は精度1.00のセルが並ぶ。ただしk=pとなる対角のセル(p=100・k=100、p=316・k=316、p=1000・k=1000)だけは0.51〜0.52と白く抜けている。下の正しいCVのヒートマップは全セルが0.48〜0.55の範囲で薄いままになっている

上のヒートマップ(リークあり)の右上、p≥10,000p\geq10{,}000かつk≥316k\geq316の領域では、交差検証精度が1.000——つまり100サンプル全問正解——になっている。p=10,000,k=316p=10{,}000, k=316のセルは、20シードのすべてで一度もミスなく1.000だった。もともと「精度90%が出てしまう」という想定でこの実験を始めたのだが、水増しは90%では止まらなかった。ただの乱数から、交差検証を(見かけ上)完璧に通過するモデルが安定して作れてしまう。

そしてもう1つ、このヒートマップには面白い「抜け」がある。k=pk=pとなる対角のセル——つまり全特徴を選ぶ(=実質何も選択しない)場合——だけは、p=100p=100で0.505、p=316p=316で0.522、p=1,000p=1{,}000で0.511と、きれいに偶然の水準へ戻るのだ。リークの正体が「テストラベルを使った選別という行為」そのものであることが、この対角線にはっきり現れている。選別がなければ、同じ手順でも幻は生まれない。

結果3: 分布ごと見る — 正しいCVとリークの間に開いていく溝

シード平均だけでなく、60シードぶんの精度の分布そのものがppとともにどう動くかもアニメーションにした(k=10k=10固定)。

k=10に固定し、特徴数pが100から100,000まで13段階で増えていくアニメーション。横軸は5-fold交差検証精度、縦軸はシード数(全60)のヒストグラムで、青が正しいCV(fold内で選択)、オレンジがリークあり(全データで選択)。p=100の時点で既に青の分布は0.5の破線を中心に山を作り、オレンジの分布は0.59〜0.79の範囲にあってほとんど重ならない。pが増えるにつれオレンジの山だけがどんどん右へ滑っていき、p=100,000では0.81〜0.96の範囲に達する。青の山は最後まで0.5周辺から動かない。タイトルには現在のpとリークあり平均精度が表示される

p=100p=100の時点で、2つの山はすでに大きく離れている(リークあり側の60シードの最小値は0.59で、正しいCV側でそこに届いたのは60シード中7つだけ)。そしてppが増えると、オレンジの山だけが右へ右へと滑っていく。p=100,000p=100{,}000では両者は1シードも重ならないところまで完全に分離し、リークありの精度は最悪のシードでも0.81、最良では0.96に達する(正しいCV側の最大は0.71)。「運悪くリークしても、たまたま低い精度が出て気づける」という救済はほぼ期待できないことが分布から読み取れる。

対照実験:標準化の影響は今回の設定では小さかった

「同じ数字を見ているのに結論が逆になる」という点ではSimpsonのパラドックスの回と同じ種類の罠だが、あちらが集計単位の問題だったのに対し、こちらは手順の順番だけで起きる。さて、リークと聞いてもう1つよく槍玉に挙がるのが「StandardScalerを交差検証の外でfitしてしまう」パターンだ。scikit-learnのドキュメントでもPipelineを使えと繰り返し警告される。では、その害は特徴選択リークとどのくらい違うのか。

縦にスタックした2枚の散布図。どちらも横軸が正しいCVの精度、縦軸がリークありCVの精度で、同じデータ・同じ設定のペアを全条件(p×k×20シード)について1点ずつ打ち、グレーの破線でy=xの対角線が引かれている。上の散布図(特徴選択リーク、オレンジ)では点の大群が対角線から大きく上に浮き上がり、精度1.0の上端に張り付く点も多数ある。対角線上に乗っているのはk=pで選択が無意味になる一部の点だけ。下の散布図(標準化リーク、緑)では全ての点がy=xの対角線にぴったり張り付いた細い帯になっていて、上下への系統的なズレが見えない

全640条件(p×k×20シード)で標準化リークあり/なしの精度差を取ると、平均+0.0001、差の標準偏差0.013、最大0.08だった。平均差は小さいが、「影響がないことを証明した」わけではない。特徴選択リークが同じ比較で平均+0.396(最大+0.64)の水増しを生んだのとは、規模が大きく違った。

今回の特徴は標準正規乱数で、全体と訓練foldの平均・分散が大きく違いにくい。一方、特徴選択では検証用のラベルそのものを使う。この違いが結果に表れていると考えられる。ただし、ラベルを使わなくても検証データの分布を先に知ることはリークになる。影響の大小を比べる話と、評価手順を正しく保つ話は分けたい。

手元のコードを直す:特徴選択も標準化もPipelineに入れる

以下は手順を示す最小例だ。本文の相関による選択を、2クラスの一変量検定f_classifで実装している。本文の20シード実験の数値をそのまま再現するコードではない。

import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

rng = np.random.default_rng(42)
X = rng.normal(size=(100, 10_000))
y = rng.integers(0, 2, size=100)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

model = make_pipeline(
    SelectKBest(score_func=f_classif, k=100),
    StandardScaler(),
    LogisticRegression(max_iter=2000),
)
# 各foldの訓練側だけで選択・標準化・学習が行われる
scores = cross_val_score(model, X, y, cv=cv, scoring="accuracy")
print(scores, scores.mean())

失敗するのは、SelectKBest(...).fit_transform(X, y)をこの交差検証の前に実行する形だ。scikit-learn公式のリーク解説も、分割後に前処理を学習し、Pipeline全体を評価する手順を示している。

さらに、特徴数kを何通りも試して一番高かったCV精度をそのまま報告すると、選んだ分の楽観的な偏りが残る。ネストした交差検証なら、内側で設定を選び、外側で評価できる。未使用のテストセットを最終評価に残す方法もある。同じ人の複数行や時系列データなら、Pipelineに加えて人単位・時間順の分割も必要になる。

正直に書いておくべきこと

  • 正しいCVの精度も0.5ちょうどにはならない。 セル平均は0.483〜0.546の範囲で揺れた(全体平均0.511)。n=100n=100の交差検証1回の精度は標準偏差にして0.05前後揺らぐので、この程度のズレは期待通りの雑音だ。p=3,162p=3{,}162の行が軒並み0.53〜0.54と高めに見えるのも、同じ20個のデータセットをkkを変えて使い回しているため行内の値が相関しているせいで、意味のある構造ではない。
  • 「標準化リークは無害」はこの設定での話。 特徴が行儀のよいガウス乱数で、分類器がスケールにほぼ不変なロジスティック回帰、という条件下の結果だ。裾の重い特徴や外れ値、nnがさらに小さい場合、あるいは目的変数を使う前処理(ターゲットエンコーディングなど)ではこの結論は使えない。今回の実験が示すのは「前処理リークが常に無害」ではなく、「リークの害はリークの種類によって桁違いに異なる」ことだ。
  • 分類器はロジスティック回帰のみ、選択基準は相関のみ。 勾配ブースティングでの選択リークや、相互情報量ベースの選択などは測っていない。ただし水増しの主因は分類器ではなく「pp個の中から偶然の当たりを選べること」なので、傾向は大きくは変わらないと考えている(これは実測していない推測だ)。
  • 交差検証の分割数(5-fold固定)やラベルの偏りの影響も振っていない。

手を動かして意外だったこと

一番の想定外は、水増しの上限だった。始める前は「乱数から精度90%が出たら記事になる」と思っていたのだが、実際にはp≥10,000p\geq10{,}000・k≥316k\geq316の広い領域で精度は1.000に到達し、20シードの1つも欠けることなく全問正解が続いた。100サンプルを5つのfoldに分けて、どのfoldをテストにしても100発100中——リークした選別が作る「偶然の当たり特徴の寄せ集め」は、私が思っていたよりはるかに強力な暗記装置だった。

もう1つはkkの効き方だ。やる前は「選ぶ特徴を増やせば、1個あたりの偶然相関は弱くなるから水増しは薄まる」と直感していたが、逆だった。p=100,000p=100{,}000ではk=10k=10の0.883よりk=316k=316の1.000の方がひどい。弱い偶然相関でも、テストラベルを見て選ばれた特徴を316本束ねれば、ロジスティック回帰は完璧な暗記を組み上げてしまう。それでいてk=pk=pまで増やした瞬間に幻は消滅する。「選ぶ数を増やすほど悪化するが、全部選ぶと無害」という非単調な形は、ヒートマップを描いて初めて腹落ちした。

そして対照実験の標準化リークが、ここまで平均差の小さい結果になるとは思っていなかった。実験前は「特徴選択より小さいが、目に見える程度の水増しはあるだろう」と予想していたので、平均+0.0001という数字は予想と違った点として記録しておく。

まとめ

  • n=100の純粋なガウス乱数と独立なランダムラベル(未知データでの精度の期待値は0.5)に対し、特徴選択を交差検証の外で行うと、5-fold CV精度はp=100p=100でも平均0.676、p=100,000p=100{,}000では0.883(k=10k=10)に達した
  • 選ぶ特徴数を増やすと水増しはさらに悪化し、p≥10,000p\geq10{,}000・k≥316k\geq316の領域では20シードすべてでCV精度1.000——乱数から全問正解のモデルが安定して作れた
  • ただしk=pk=p(全特徴を選ぶ=選択しない)にすると精度は0.505〜0.522と偶然の水準に戻った。害の正体はテストラベルを使った「選別という行為」そのものだった
  • 同じ設定で標準化リークの平均差は+0.0001と小さかった。ただし、標準化も訓練fold内で学習する原則は変わらない
  • 幻の精度はppとともに2ln⁡p/n\sqrt{2\ln p/n}のペースで伸びる偶然相関の最大値に支えられており、「特徴が多いほど、リークしたときの見かけの成績はむしろ良くなる」——高次元データほど、この落とし穴は深くなる