9月21日、Towards Data Scienceが「Your AI Assistant Wrote the Code. Who Checked the Defaults?」と題した記事を公開した。AIコーディングアシスタントが生成するscikit-learnコードに潜む「デフォルト引数の罠」5選を取り上げており、いずれも「バグではなく、エラーも出ないが、意図と違うことが起きている」タイプの問題だ。
AIアシスタントに「ランダムフォレストを書いて」と頼むと、動くコードが返ってくる。インポートは正しく、フィットも通り、予測結果の形状も期待通り。だが、そのコードが「指定しなかった引数」でいくつもの設計判断を暗黙に下していることは、コードを見ただけでは気づかない。
GitHub CopilotやChatGPTといったAIコーディングツールの普及により、機械学習の実装コストは大幅に下がった。一方で、「動くコードを生成すること」と「設計判断を下すこと」は別物だという認識が現場で追いつかないケースも増えている。本記事の著者が指摘するのは、まさにその盲点だ。
本記事の著者は、自身と同僚のプロダクション環境で実際にデバッグコストを生んだデフォルト設定を5つ取り上げている。
最も見落とされがちな罠:RandomForestRegressorのmax_features
5つのうち最もインパクトが大きいのが、**RandomForestRegressorのmax_featuresデフォルト値**だ。
「ランダムフォレストは行をブートストラップし、特徴量をサブサンプリングし、複数の木を平均する」と学んだ人には驚きがある。RandomForestClassifierのデフォルトはmax_features="sqrt"(特徴量数の平方根)なのに対し、**RandomForestRegressorのデフォルトはmax_features=1.0、つまり全特徴量を使う**。
これは何を意味するか。特徴量のサブサンプリングがオフになっており、ランダムフォレストを「ただのバギング」と区別する第二の乱数源が消えている。
販売モデルで「価格」のような強い予測変数が1つあるとする。各木はブートストラップで異なる行を受け取るが、価格の予測力がそれに耐えるため、どの木も初期の分岐で価格を選び続ける。木の構造が似通い、木を増やしても相関由来の分散が減らない。特徴量のサブサンプリングはその繰り返しを断ち切る仕組みだが、max_features=1.0ではその機構が使われない。
max_features=0.33のように設定すると、各分岐で約1/3の特徴量のみを候補にする形に戻せる。
ロジスティック回帰の正則化:C=1.0は「無検討の正則化」
LogisticRegression()はデフォルトでL2正則化を適用し、その強さを制御するパラメータCのデフォルトは1.0だ。Cが小さいほど正則化が強く、大きいほど係数に自由度を与える。1.0はあなたのデータに特別な根拠を持つ値ではない。
さらに微妙な問題がある。正則化の大きさは係数の大きさに依存し、係数の大きさはスケールに依存する。収入を「ユーロ」で記録するか「千ユーロ」で記録するかで係数は1000倍変わり、L2ペナルティへの寄与は100万倍変わる。同じCでリフィットすれば、単位を変えただけで正則化の効き方が変わる。
対策は、連続特徴量をPipeline内で標準化し、各学習フォールド内でスケーリングを完結させたうえでCをチューニングすることだ。
cross_val_scoreのcv=5:「何回評価するか」しか指定していない
cross_val_score(model, X, y, cv=5)のcv=5は分割数を指定するだけで、分割の構造をscikit-learnに委ねている。
回帰タスクではshuffle=FalseのKFoldが使われる。データフレームが日付順に並んでいれば、バリデーションの1折目は「行1〜20で評価、行21〜100で学習」となり、未来のデータで過去を予測する形になる。コードは正常に動くが、その評価はForecasting(時系列予測)を全く再現していない。
著者が推奨する考え方:
- 行の順序が無意味な独立観測値:シャッフルありの
KFold - 時系列予測:過去で学習し未来で評価する時間ベースの分割
- 顧客ごとの予測:同一顧客が学習とバリデーションに混在しない
GroupKFold
cv=5は「評価を何回繰り返すか」を決めたにすぎない。「何に汎化させたいか」の定義はまだされていない。
KMeansのn_init="auto":試行回数が意図通りになっているか
KMeans(n_init="auto")の"auto"は安心感を与える文字列だが、その挙動は初期化方法との組み合わせで決まる。**init="k-means++"(デフォルト)との組み合わせでは、scikit-learnの公式仕様によりn_initは1に設定される**(公式ドキュメント参照)。つまり実質的に初期化を1回しか試みない。
KMeansは初期クラスタ中心の配置によって局所最適解に収束しうる。n_init=10なら10回試して最も慣性(inertia)の低い結果を採用する。"auto"はその試行回数を「初期化方法に基づく固定ルール」で決めるだけで、結果の安定性を確認してリスタートしているわけではない。
max_iterを増やしても各実行内のイテレーション数が増えるだけで、複数試行の代替にはならない。明示的にn_init=10と書くことで初めて複数試行が有効になる。
SimpleImputer:欠損値を埋めるつもりが列を削除している
SimpleImputer()をパイプラインに追加すると、欠損値を補完するだけでなく、学習データで全値が欠損している列を丸ごと削除する(デフォルトのkeep_empty_features=False)。
20特徴量で学習したとき、そのうち1列が全欠損ならImputerは19特徴量に変換する。本番データでその列に実値が入っていても、学習時の判断でその列は消える。特徴量数エラーも出ない——パイプライン全体で一貫して19特徴量を扱っているため、.predict()は正常に完了する。
この問題が厄介なのは、エラーが無声で起きる点だ。たとえば学習時にセンサー障害でA列が全欠損だったとする。本番ではA列が復旧し実値が入るようになっても、パイプラインはその列を存在しないものとして処理し続ける。モデルの精度劣化として現れるまで、問題の所在すら特定しにくい。
keep_empty_features=Trueを設定すると列を保持し、欠損値をゼロで埋める。ただし、学習データにその列の実値がなかった事実は変わらないため、保持したからといってモデルがその列を有意義に活用できるわけではない。重要なのは、前処理後の特徴量数と名称を実際に確認する習慣だ。
結論:「コードを書いて」は「設計を決めて」とは違う
5つのデフォルトに共通するのは、「バグではない」「エラーも出ない」「でも意図と違うことが起きている」という点だ。AIアシスタントは動くコードを生成できるが、「行の順序は関係あるか」「初期化を複数試すべきか」「どの特徴量が前処理後に残るか」という問いは、プロンプトに書かれていなければ問われない。
デフォルト値を確認し、「なぜそれが自分のデータに合うか」を言語化する——それはコードを書く作業とは別の判断だ。各パラメータの意味を把握するうえでは、scikit-learnの公式APIリファレンスを手元に置いておくとよい。
詳細はYour AI Assistant Wrote the Code. Who Checked the Defaults?を参照していただきたい。