Claude Media
Claude Codeで機械学習のデータリークを点検する — fit位置と分割の見直し

Claude Codeで機械学習のデータリークを点検する — fit位置と分割の見直し

前処理のfitが検証データに及んでいないか、時系列や同一ユーザーの分割は妥当かを、読み取り専用のサブエージェントに点検させる手順。疑わしい箇所は分け直して数値で確かめます。

学習コードのデータリークは、実行してもエラーが出ません。検証スコアが少し高く出るだけなので、本番に出してから気づくことになりがちです。Claude Codeの読み取り専用サブエージェントに、前処理のfit位置、時系列の分割、同一ユーザーのまたぎ、特徴量の未来情報を観点別に点検させ、疑わしい箇所は分け直した再実験で数値の落ち幅を見る。この記事はその手順です。

データリークは「予測時に手に入らない情報」の混入

データリークとは、予測の時点では手に入らない情報をモデルの構築に使ってしまうことです。scikit-learnのドキュメントは、その結果として交差検証などの性能推定が楽観的になり、実データでの性能が悪化すると説明しています。

典型的な原因は、学習用と評価用のデータが分かれていないことです。ドキュメントの基本ルールは「テストデータに対してfitを呼ばない」で、正規化なら平均は学習用データだけから計算します。評価用データを混ぜた平均を使うと、評価用データの情報がモデルに流れ込みます。

このミスはコードの形で見つけられます。fitやfit_transformの引数に何が渡っているか、分割より前に前処理をしていないか、を上から追うだけです。レビュー向きの作業なので、サブエージェントに任せる価値があります。

実装そのものはClaude Codeでscikit-learnモデルを実装する手順で扱っています。ここでは書き終えたコードを疑う側の話をします。

書き込めないレビュー役のサブエージェントを作る

点検役は、コードを読むだけで直さない設計にします。サブエージェントは.claude/agents/にMarkdownファイルを置いて定義し、toolsで使えるツールを絞れます。toolsを省略すると、サブエージェントが使えるツールをすべて継承します。省略しないことが読み取り専用の条件です。

mkdir -p .claude/agents

.claude/agents/leakage-reviewer.mdを次の内容で作ります。

---
name: leakage-reviewer
description: 機械学習の学習コードを読み、データリークの疑いを観点別に洗い出す。コードは変更しない。
tools: Read, Grep, Glob
---
 
あなたは機械学習コードのデータリークを点検するレビュー担当です。
指定されたファイルを読み、次の4観点で疑わしい箇所を挙げてください。
 
1. 前処理の fit 位置: scaler / encoder / imputer / PCA / 特徴量選択の
   fit や fit_transform が、分割前の全データや評価用データに
   かかっていないか。Pipeline に入っているか。
2. 時系列の分割: 時間順のデータをシャッフルして分割していないか。
   評価用データが学習用データより未来になっているか。
3. 同一対象のまたぎ: 同じユーザー・患者・店舗などの行が
   学習側と評価側の両方に入っていないか。groups を指定しているか。
4. 特徴量の未来情報: 予測時点では手に入らない列
   (結果の後に確定する値、目的変数から作った集計など)がないか。
 
出力は観点ごとに「ファイル:行番号 / 疑う理由 / 確かめ方」の表にします。
ファイルを編集せず、実行もしません。断定できないものは「疑い」と書き、
確かめる再実験の案を添えてください。

Read, Grep, Globだけを許可しているので、このサブエージェントはファイルの編集も、コマンドの実行もできません。Bashを加えるとコードを実行できますが、それは次の再実験でメインの会話側が担当します。

公式のサンプルにもRead, Grep, Glob, Bashで読み取り中心のコードレビュー役を作る例があります。本記事の定義は、そこからBashを外して実行もさせない形です。

Supabaseのレビュー用サブエージェントも同じ考え方で、読む役と直す役を分けています。DB分析役でツールの絞り込みをhookまで広げたい場合は読み取り専用のDB分析役の作り方が参考になります。

4つの観点で何を疑うか

プロンプトに書いた4観点を、疑う書き方と直し方に分けて並べます。

観点疑う書き方直し方
前処理のfit位置疑う書き方分割前にscaler.fit_transform(X)、SelectKBest(...).fit_transform(X, y)直し方先に分割し、学習側だけにfit。Pipelineに入れる
時系列の分割疑う書き方時間順データにシャッフルありのKFoldやtrain_test_split直し方TimeSeriesSplitなど、未来を評価側に置く分割
同一対象のまたぎ疑う書き方患者や利用者の複数行をランダムに分割直し方GroupKFoldでgroupsにIDを渡す
特徴量の未来情報疑う書き方予測時点より後に確定する列、目的変数から作った集計直し方列の生成時点を確認して除く

fit位置は前処理の種類を問わない

ドキュメントは特徴量選択を例にしていますが、この危険はStandardScaler、SimpleImputer、PCAを含むほぼすべての変換にあると注記しています。エンコーダーも同じ型なので、レビューでは変換器の名前でなく「fit系の呼び出しに渡している引数」で追うよう、プロンプトに書いてあります。

時系列とグループは分割器で見分ける

通常のKFoldやShuffleSplitは、サンプルが独立同分布だと仮定しています。時系列では近い時刻のサンプルが相関するため、評価が不当に甘くなります。ドキュメントは、未来の観測で評価するためにTimeSeriesSplitを示しています。

同一対象のまたぎは、ドキュメントが患者IDの例で説明しています。1人から複数のサンプルを取ったデータでは、検証側の行がすべて学習側に現れないグループから来るようにします。そのためにGroupKFoldを使い、groups引数にIDを渡します。

未来情報は列の意味を知らないと見つからない

4つ目は、コードだけでは判定しにくい観点です。列名からは「これは結果の後に確定する値かもしれない」という疑いしか立てられません。サブエージェントの出力は「疑い」に留め、列の生成タイミングは担当者に確認します。この確認は、プロンプトの最後の一文で求めています。

レビューを走らせて、結果を読む

名前を指定して呼び出す方法は3つあります。自然文で名前を出す、@で選ぶ、--agentでセッション全体をそのサブエージェントにする、です。確実に動かすなら@が向いています。

@"leakage-reviewer (agent)" train.py と features.py を点検して

@で指定しても、メッセージ全体はClaudeに届き、サブエージェントへの依頼文はClaudeが書きます。点検してほしいファイル名は、プロンプトに明記しておきます。

返ってくる報告は、たとえば次のような形になります。以下は形式の例で、実際の出力ではありません。

観点1: train.py:42 / 分割前に全データへ StandardScaler.fit_transform /
       評価用データの平均・分散が学習側に入る疑い /
       train_test_split の後に fit し直して比較
観点3: train.py:55 / user_id が複数行あるが KFold を使用 /
       同一ユーザーが両側に入る疑い / GroupKFold(groups=user_id) で比較

ここで出た指摘は、まだ仮説です。次の節で、本当にスコアが動くかを確かめます。

疑わしい箇所は分け直して数値の落ち幅を見る

指摘が正しければ、直した後の検証スコアは下がります。下がった幅が、リークで水増しされていた分です。再実験はメインの会話で行います。実行権限を持たないレビュー役に、スクリプトを走らせる必要はありません。

メインのClaude Codeに、次のように頼みます。

leakage-reviewer の指摘 1 と 3 について、現在の分割と、
Pipeline + GroupKFold に直した分割で検証スコアを比較する
スクリプト compare_split.py を作って実行して。コードの本体は変えないで

比較の骨格は次のとおりです。リークがある分割と、ない分割を同じ指標で並べます。

import numpy as np
from sklearn.model_selection import GroupKFold, KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import HistGradientBoostingClassifier
 
# X, y, user_id は自分のデータに差し替える
pipe = make_pipeline(
    StandardScaler(),
    HistGradientBoostingClassifier(random_state=1),
)
 
before = cross_val_score(pipe, X, y, cv=KFold(5, shuffle=True, random_state=0))
after = cross_val_score(
    pipe, X, y, groups=user_id, cv=GroupKFold(n_splits=5)
)
print(f"行ランダム分割 : {before.mean():.3f}")
print(f"ユーザー単位   : {after.mean():.3f}")

Pipelineに入れておけば、交差検証の各分割で学習側だけにfitされます。変更するのは分割器とgroupsだけなので、スコアの差を分割の違いに帰属できます。時系列ならcvをTimeSeriesSplitに替えます。

どれくらい落ちるものか

scikit-learnのドキュメントには、リークの大きさが分かる例があります。ランダムな特徴量1万個とランダムな目的変数のデータで、全データに特徴量選択をかけてから分割すると、正解率は0.76になります。目的変数は完全にランダムなので、本来は0.5前後のはずの値です。

先に分割し、学習側だけで特徴量選択をすると正解率は0.5になり、Pipelineをcross_val_scoreに渡すと平均0.43です。この例の落ち幅は大きいものですが、実データでは数ポイントしか落ちないこともあります。落ち幅が小さくても、直す意味は変わりません。

落ち幅の読み方

  • ほとんど変わらない: その指摘は、少なくともこのデータでは影響が小さい
  • 大きく落ちる: 本番でも同程度に落ちると見て、直した分割を標準にする
  • 想定より高いまま: 別のリークが残っている可能性があり、観点4の列を見直す

Pipelineに入れても残るリークと、点検の補助線

Pipelineが守るのは、パイプラインの中に入った変換器のfitだけです。分割より前にDataFrame全体で計算した列は、パイプラインの外にあります。たとえば全行を使ったカテゴリ別の目的変数の平均や、全期間の統計量で作った列です。これらはfitという名前の呼び出しを持たないため、観点1の機械的な検索には引っかかりません。

そこで、レビュー役に渡す前にメイン側でfit系の呼び出し位置だけ一覧にしておくと、指摘の根拠を行番号で追えます。

grep -n "fit_transform\|\.fit(\|train_test_split\|Pipeline" *.py

一覧の中で、train_test_splitの行番号より前にあるfit系の呼び出しが、観点1の最有力候補です。groupbyやrollingのようにfitを持たない集計は、観点4のプロンプトで「分割前に全データで計算した集計列」と明記して拾わせます。

点検を毎回の習慣にする

レビュー役のdescriptionに「use proactively」のような語を入れると、Claudeが自発的に委譲しやすくなります。プロジェクトのCLAUDE.mdにも、学習コードを変えたら点検に回すことを短く書いておけます。

## 機械学習コードの変更時
- 分割・前処理・特徴量を変えたら leakage-reviewer に点検させる
- 指摘は仮説として扱い、分割を直した再実験のスコアで採否を決める
- 評価用データには fit 系の呼び出しを使わない

ルールを文章で置くだけなので強制力はありません。確実に止めたい場合は、train_test_splitより前のfitをテストで検知するなど、別の仕組みが要ります。

評価用データでモデルの選択をしていないか

リークはfitだけではありません。scikit-learnのドキュメントは、テストデータをモデルに関する選択に使ってはならないとも述べています。評価用データのスコアを見てハイパーパラメータや判定のしきい値を決めると、評価用データが調整に使われたことになります。

この型もコードの形で拾えます。評価用データのpredictやscoreの結果を、パラメータを変えるforループの中で参照していないかを読めば足ります。4観点の下に5つ目として、プロンプトに1行足せます。

5. 評価用データでの選択: 評価用データのスコアを見て、
   ハイパーパラメータやしきい値を決めていないか。

選択用には学習データから検証用を切り出し、評価用データは最後の1回だけ使う運用にします。

レビュー役を使うときの注意

サブエージェントは新しい文脈で動き、依頼文とシステムプロンプトに書かれた情報だけで判断します。データの意味(どの列がいつ確定するか)は、依頼文に書かない限り伝わりません。列ごとの生成タイミングを短いメモにして、点検対象のファイルと一緒に渡すと、観点4の精度が上がります。

もうひとつ、指摘の数と深刻度は一致しません。軽い指摘が並んで本命が埋もれることがあるので、プロンプトでは観点ごとの表と確かめ方を求めています。

学習データ側の検証は別の道具が向いています。JSONLの前処理を型で固定する方法はpanderaでスキーマを固定する手順にあります。

まとめ

データリークはエラーにならないので、人の目かレビュー役の目が要ります。読み取り専用のサブエージェントに4観点を点検させ、指摘は仮説として受け取る。そのうえで、分割を直した再実験の数値で採否を決める。レビューと実行を別の役に分けると、「直したつもり」で終わらず、スコアの落ち幅という証拠が残ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/sub-agents
  2. scikit-learn.org/stable/common_pitfalls.html
  3. scikit-learn.org/stable/modules/cross_validation.html
この記事を共有:XはてブLinkedIn