Claude Media
Claude Codeでscikit-learnモデルを実装する — fit/predict/評価まで

Claude Codeでscikit-learnモデルを実装する — fit/predict/評価まで

scikit-learnのfit/predict/評価をClaude Codeに実装させる手順と、データリークなどのつまずきどころをまとめます。

scikit-learnのモデル実装は、推定器を選び、fitで学習させ、predictで予測し、交差検証で評価するという決まった流れをたどります。Claude Codeはこの流れをコードとして書くだけでなく、Bashツールで実際に実行して結果を確認しながら進められます。この記事ではfit/predictの基本からPipeline、評価、ハイパーパラメータ探索までを、Claude Codeに何を渡すと迷わず実装できるかという観点でまとめます。

Claude Codeとscikit-learnを組み合わせる基本の流れ

scikit-learnは、教師あり学習と教師なし学習のどちらも扱えるPythonの機械学習ライブラリです。推定器(estimator)と呼ばれるオブジェクトがfit・predict・transformという共通のAPIを持っており、モデルの種類が変わってもコードの型は変わりません。

Claude Codeにこの実装を任せるときの利点は、コードを提案するだけで終わらない点にあります。Bashツールでpythonコマンドを直接実行できるため、fitが正常に終わるか、predictの出力が期待した形か、その場で確認しながら次のステップに進めます。人間が毎回コピーして手元で実行し直す手間が減ります。

ただし実行できることと、正しいコードになることは別問題です。スケーリングを分割前にかけてしまうデータリークのように、動くけれど評価結果が水増しされるコードも「エラーなく実行できた」という理由でそのまま採用されがちです。各段階で何を渡し、何を見るかは次の4ステップになります。

流れ

実装を任せるときの4ステップ

  1. 1

    fit/predictの最小形を書かせる

    形と戻り値を見て、Xとyの並びが想定どおりかを確かめます。

  2. 2

    前処理をPipelineに包ませる

    fitが呼ばれる箇所を挙げさせ、全データにfitしている行がないかを見ます。

  3. 3

    分割ごとのスコアを出させる

    単一のスコアでなく、cross_validateが返す配列で分割間のばらつきを見ます。

  4. 4

    探索はPipeline全体に対して回す

    前処理込みのオブジェクトをRandomizedSearchCVに渡しているかを見ます。

前提

この記事の手順は、次の環境を前提に進めます。

  • Python環境にscikit-learnがインストール済みであること(pip install scikit-learn)
  • Claude Codeがプロジェクトディレクトリ内で起動していること
  • 手順4のコードは、scipy.statsの分布を使うためSciPyも必要です(scikit-learnの依存として入ります)

プロジェクト直下にCLAUDE.mdを置き、「学習と評価の前処理は必ずPipelineに包む」「乱数シードはrandom_state=0で固定する」といった規約を書いておくと、Claude Codeが実装ごとに同じ判断で迷わずに書けます。CLAUDE.mdの具体的な書き方はClaude CodeのCLAUDE.mdを実用に引き上げる10のパターンにまとまっています。

手順1: fit/predictの基本をClaude Codeに実装させる

推定器のfitとpredictは、scikit-learnのすべてのモデルに共通する最小単位です。まずはClaude Codeにこの基本形を書かせて、動作の感触を掴みます。

RandomForestClassifierを使って、サンプルの2次元配列Xとラベルyでfitし、
predictで新しいデータのクラスを予測するスクリプトを書いて。random_stateは0で固定して。

scikit-learn公式のGetting Startedで示されている最小構成は次のとおりです。

from sklearn.ensemble import RandomForestClassifier
 
clf = RandomForestClassifier(random_state=0)
X = [[1, 2, 3], [11, 12, 13]]  # 2サンプル、3特徴量
y = [0, 1]  # 各サンプルのクラス
clf.fit(X, y)
 
clf.predict(X)  # 学習データのクラスを予測
clf.predict([[4, 5, 6], [14, 15, 16]])  # 新しいデータのクラスを予測

fitは基本的に2つの入力を受け取ります。サンプル行列X(行がサンプル、列が特徴量)と、目的変数yです。yは分類なら整数などの離散値、回帰なら実数で、教師なし学習では省略できます。一度fitしたあとは、predictを呼ぶたびに再学習する必要はありません。

手順2: StandardScalerとPipelineでデータリークを防ぐ

実務のモデル実装は、fit/predictだけでは終わりません。前処理(スケーリングや欠損値の補完)と推定器を組み合わせるのが普通です。scikit-learnでは前処理を担う変換器(transformer)も推定器と同じAPIに従い、fitの代わりにtransformで変換後のデータを返します。

分割前にデータ全体へfitするとテストデータの情報が学習側へ漏れます(データリーク)。これを防ぐ手段がPipelineです。

StandardScalerとLogisticRegressionをPipelineでつないで、
Irisデータセットをtrain_test_splitで分割してから学習し、
テストデータでの正解率をaccuracy_scoreで出して。
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
 
pipe = make_pipeline(StandardScaler(), LogisticRegression())
 
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
 
pipe.fit(X_train, y_train)
accuracy_score(pipe.predict(X_test), y_test)  # 約0.97

Pipelineは変換器と推定器をひとまとめにし、通常の推定器と同じfit・predictのAPIで扱えるようにします。分割後の学習データだけでスケーリングのパラメータを計算するため、データリークを構造的に防げます。特徴量ごとに異なる前処理をかけたい場合は、ColumnTransformerで列ごとに変換を振り分けられます。

データリークで点数はどこまで膨らむか

リークは「少し甘くなる」程度ではありません。scikit-learn公式の落とし穴ページには、特徴量選択でリークを再現した例があります。サンプル200件・特徴量10,000個・ラベルも完全な乱数にしたデータで、特徴量を25個に絞ります。ラベルが乱数なので、正しい評価なら正解率は0.5前後のはずです。

くらべる

同じ乱数データでの正解率

リークあり

全データで特徴量選択してから分割

SelectKBest(k=25).fit_transform(X, y)を分割の前にかけると、正解率は0.76になります。

リークなし

分割してから学習データだけで選択

学習データだけで選び、テストデータにはtransformだけをかけると、正解率は0.5です。

公式は、この種のリークがStandardScaler、SimpleImputer、PCAなどほぼすべての変換で起こりうると書いています。守る規則は3つです。先に分割する。テストデータにはfit・fit_transformを呼ばない。テストデータには学習データで得た変換をtransformで当てる。

Claude Codeに依頼するときは、書かせた後に次の点検を頼むと漏れを拾えます。

作ったスクリプトの中で、fit / fit_transform が呼ばれている行を全部挙げて。
それぞれ、学習データだけに対して呼ばれているかを確認して。

出力された行のうち、引数が分割前のXになっているものがリークの疑いです。

手順3: cross_validateで汎化性能を評価する

train_test_splitによる1回の分割だけでは、その分割がたまたま良かった可能性を排除できません。scikit-learnは交差検証のためのcross_validateを提供しており、既定で5分割の評価を行います。

LinearRegressionでmake_regressionの合成データを学習して、
cross_validateで5分割交差検証のtest_scoreを出して。
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_validate
 
X, y = make_regression(n_samples=1000, random_state=0)
lr = LinearRegression()
 
result = cross_validate(lr, X, y)  # 既定で5分割交差検証
result["test_score"]

cross_validateが返すのは辞書で、キーはfit_time・score_time・test_scoreです。train_scoreはreturn_train_score=Trueを渡したときだけ入ります。LinearRegressionの既定のスコアはR²で、公式の例では合成データが易しいため、5分割すべてが1.0になります。scoringに複数の指標を渡すと、キーはtest_r2のように指標名入りに変わります。

分割の方法も既定のまま動きます。cvを省くと5分割で、分類器でyが2値か多クラスならStratifiedKFold、それ以外はKFoldが使われます。どちらもshuffle=Falseなので、データの並び順がそのまま分割に反映されます。

分割ごとのスコア(result["test_score"])を出させると、モデルが分割によってどれだけブレるかが見えます。単一のスコアだけを報告させると、たまたま良かった分割を採用してしまうリスクが残るため、Claude Codeへの依頼では「分割ごとのスコアも出して」と明示しておくと確認しやすくなります。

スコアの数値だけでは前処理コード自体のバグまでは見抜けません。「変換後のデータは元と同じ行数を保つ」のような性質を検証したい場合は、性質ベースのテストも組み合わせられます(ClaudeとProperty-Based TestingでPythonのバグを見つける仕組み)。

手順4: RandomizedSearchCVでハイパーパラメータを探索する

すべての推定器はハイパーパラメータを持ち、その値によって汎化性能が大きく変わります。適切な値はデータに依存するため、経験則だけで決め切るのは現実的ではありません。scikit-learnはRandomizedSearchCVで、交差検証を使ったパラメータ探索を自動化できます。

from sklearn.datasets import make_regression
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import RandomizedSearchCV, train_test_split
from scipy.stats import randint
 
X, y = make_regression(n_samples=20640, n_features=8, noise=0.1, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
 
param_distributions = {
    "n_estimators": randint(1, 5),
    "max_depth": randint(5, 10),
}
 
search = RandomizedSearchCV(
    estimator=RandomForestRegressor(random_state=0),
    n_iter=5,
    param_distributions=param_distributions,
    random_state=0,
)
search.fit(X_train, y_train)
search.best_params_  # 例: {"max_depth": 9, "n_estimators": 4}
search.score(X_test, y_test)

探索が終わると、RandomizedSearchCVオブジェクト自体が最良パラメータで学習済みの推定器として振る舞います。公式のGetting Startedには、実務では単一の推定器でなくPipelineを探索対象にすることがほとんどだという注記があります。データ全体に前処理をかけてから交差検証すると、テストデータの情報が学習側に漏れ、汎化性能を過大に見積もるためです。落とし穴ページも、Pipelineは交差検証とハイパーパラメータ探索の関数に渡すのに向いていると説明しています。

探索空間をどう渡すか

param_distributionsはパラメータ名と分布のペアで指定します。整数の範囲ならscipy.stats.randint、連続値ならscipy.stats.uniformのように、値の性質に合った分布を選びます。Claude Codeに探索を依頼するときは、パラメータ名だけでなく「木の数は1〜5、深さは5〜10の範囲で探索して」のように具体的な範囲を渡すと、根拠のない探索空間を勝手に決められずに済みます。

よくあるつまずき

  • random_stateを固定し忘れる: 既定はNoneで、実行のたびに結果が変わります。RandomStateインスタンスを渡した場合も、fitやsplitを呼ぶたびに結果が変わります。整数を渡すと何度呼んでも同じ結果になります。公式は、交差検証の分割器には整数を渡すのが最も安全だとしています。推定器については、交差検証の頑健性ならRandomStateインスタンスかNoneが勧められています。実行をまたいで同じ結果が要るときは整数にします。再現性が必要な場面では、推定器と分割の両方で整数を明示するよう依頼します。
  • Claude Codeの出力が「エラーなく動いた」で完了になる: accuracy_scoreが高い数値を返しても、その数値自体がデータリークで水増しされている場合があります。動いたことと正しく評価できていることは別で、Claude Codeデバッグの精度は、渡す情報の質で決まるで扱っている「完了条件を明示する」考え方がここでも有効です。pytestでテストを書きながら進める場合は、失敗の記録を引き継げる仕組みとしてpytestの失敗をMCPサーバーでClaudeに解析させるも参考になります。

Claude Codeのモードをどう使い分けるか

モデル実装の各段階で必要な確認の強さは異なります。着手前に構成を確認したい場面と、コードを書かせてすぐ実行させたい場面を同じモードで進める必要はありません。

場面向くモード理由
Pipelineの構成やスコアリング方針を決める段階向くモードPlanモード理由編集前に方針を確認できる。ステータスバーに⏸ plan mode onと表示され、コードはまだ書き換わらない
fit/predictや評価スクリプトを書いて実行させる段階向くモード通常モード理由コード編集とBashでの実行を都度確認しながら進められる
CIやバッチで評価結果だけを取得したい段階向くモードclaude -pによる非対話実行理由標準入力・出力を使ってスクリプトから呼び出せる。評価ログをパイプで渡して要約させることもできる

Planモードはclaude --permission-mode planで開始するか、セッション中にShift+Tabで切り替えます。v2.1.289の--helpは、次のように選択肢を表示します。

claude --help | grep -A3 -- "--permission-mode"
  --permission-mode <mode>              Permission mode to use for the session
                                        (choices: "acceptEdits", "auto",
                                        "bypassPermissions", "manual",
                                        "dontAsk", "plan")

非対話実行はgit log --oneline -20 | claude -p "summarize these recent commits"のように、Unixのパイプと同じ感覚でClaude Codeに標準入力を渡せます。評価スクリプトの実行結果をログとして貼り付け、「このログから交差検証のスコアが分割ごとにどれくらいブレているか要約して」と-p経由で渡す使い方もできます。

まとめ

数字が出たことは、評価が正しいことの証拠になりません。書かせたコードはfitが呼ばれる行を点検させてから、スコアを信用します。

この記事を共有:XはてブLinkedIn