AI ワークフローで避けるべき Python の一般的なミス 7 つ
本文の状態
日本語全文を表示中
詳細モードで約15分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
KDnuggets
本記事は、AI ワークフローにおいてコードが正常に実行されても予測結果が無効化する「沈黙のバグ」7 つを特定し、各段階での具体的な検証チェック手法を提示する。
AI深層分析を開く2026年9月1日 22:12
AI深層分析
キーポイント
データ漏洩による評価値の過大評価
前処理の学習(fit)がデータ分割前に実行されるミスにより、検証スコアが楽観的に膨れ上がる現象と、その防止策を解説する。
分割境界における関連データの混在
グループや時間軸を意識しない単純な分割により、訓練データとテストデータに同一の情報が漏洩し、新規エンティティでの性能低下を招くリスクを指摘する。
推論時の前処理パスの不整合
学習時とサービス提供時に異なる手書きの前処理コードが使用されることで、両者の出力に静かなるドリフトが生じる問題と統一されたファクスチャの重要性を説く。
再現性の誤解と評価モードの混同
シード値の設定だけで完全な再現性を保証できると信じる誤りや、検証中にドロップアウトが有効になるよう eval() と no_grad() の使い分けを間違える事例を挙げる。
データ分割前の前処理による情報リーク
特徴選択やスケーリングなどの前処理をデータ分割前に実行すると、検証用データの情報も学習に利用されてしまう。scikit-learn のパイプラインを用いて各クロスバリデーションのフォールド内で個別に変換を行うことで、この問題を回避できる。
重要な引用
The penalty then arrives as a believable number instead of a traceback.
A clean run proves the process executed. It says nothing about what the pipeline learned...
One seed treated as reproducibility
Broadcast hides a [batch, 1] vs [batch] mismatch
編集コメントを表示
編集コメント
本記事は、AI プロジェクトの品質保証において「エラーがないこと」と「結果が正しいこと」を区別する重要性を浮き彫りにしている。実務経験豊富なエンジニアにとって、既存のベストプラクティスを再確認し、潜在的な欠陥を検出するための有用なチェックリストとなるだろう。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。

検証でスコア 0.83 を取得し、ノートブックもエラー一つなく上から順に実行された。にもかかわらず、本番環境への展開から 3 週間後には予測結果が役に立たないものになってしまっていた。その間、システムがクラッシュしたことも一度もない。
これが AI ワークフローにおけるバグと、通常の Python のバグを分ける決定的な違いです。AI の API は、データや状態、形状、あるいはアーティファクトに関する契約に違反するコードであっても、平然と受け入れてしまいます。その代償として現れるのは、エラーメッセージのスタックトレースではなく、「もっともらしい数値」なのです。
クリーンな実行は「プロセスが完了した」という事実を証明するだけで、パイプラインが何を学習したのか、どの行のデータを使ったのか、どのような状態だったのか、あるいは保存された結果を他でも信頼して使えるのかといった本質的な問いには答えてくれません。
以下に挙げる 7 つの失敗はすべて「沈黙」を伴いますが、それぞれが問題の発生する境界点で検出するためのチェック方法も併せて紹介します。
| ステージ | 見落としがちなミス | 誤解を招く症状 | 確認方法 |
|---|---|---|---|
| 前処理 | 分割前に変換が適用されている | 検証スコアが楽観的に過大評価される | すべての fit 呼び出し箇所を特定し、その時点で表示されている行を確認する |
| データ分割 | 分割の両側にまたがる関連行 | 検証では強力だが、新しいエンティティでは性能が低下する | 実際の境界に合わせたグループまたは時間認識型のスプリッターを使用する |
| 推論(サービング) | 手書きの第 2 の前処理パスが存在する | 学習時と推論時の出力が静かに乖離する | 両方のパスで共通のフィクスチャを使用し、出力が同一であることをアサートする |
| ランダム性 | シードを再現性の保証と誤解している | シード付きライブラリを使用しても再実行結果が異なる | シード、データ、コード、設定、依存関係を記録する |
| 評価 | eval() と no_grad() を使い分けていない | 検証中にドロップアウトやバッチ正規化が有効になっている | ループ内で両方の呼び出しを行い、再開時に model.train() を実行する |
| 損失の境界 | ブロードキャストにより、[batch, 1] と [batch] の不一致が隠蔽される | 誤った計算による妥当な損失値 | 損失計算前に assert output.shape == target.shape を実行する |
| アーティファクト | 保存されたモデルを不活性なデータとして扱っている | ロード時にコードの実行エラーやバージョンの互換性問題が発生する | 信頼できるソースのみを使用し、推論環境でスモークテストを行う |
図1:7 つの一般的なミステイクがワークフロー内のどこに潜んでいるか、そして本番環境に到達する前にそれを暴くチェックポイント。
1. データを分割する前に前処理を行ってしまう
一度は実行すべきデモがあります。純粋なランダムノイズから 100 サンプル(特徴量 1,000 個)を用意し、コイン投げでラベルを付与します。次に SelectKBest に「最も良い」20 個の特徴を選ばせ、残った特徴量上で分類器のクロスバリデーションを実行してみましょう。
sel = SelectKBest(f_classif, k=20).fit(X, y) # fit on ALL rows
scores = cross_val_score(model, sel.transform(X), y, cv=5)学習すべき何もないデータに対してさえ、この 2 行のコードは 0.83 という精度を報告します。これは、特徴選択ステップがすべての行(後で各フォールドで「 unseen(未見)」として扱われる行も含めて)を見ていたためです。つまり、評価対象の特徴量がすでに保持されたデータの影響を受けてしまっています。
この選択処理を scikit-learn パイプライン 内に移動させ、各フォールドが独自の変換を行うようにすれば、同じ実験で精度は 0.49 に低下します。これがノイズデータに対する正直な答えです。
このルールは特徴選択に限らず、スケーリングや欠損値の補完、次元削減にも適用されます。診断には再実行ではなく検索が必要です。ワークフロー内のすべての fit と fit_transform を探し出し、その時点でどの行が参照可能だったかを特定してください。scikit-learn はこれらの落とし穴に関する カタログ を用意しており、データリークが最上位にあるのには理由があります。
2. 独立していない行をランダムに分割する
ランダム分割は「モデルが未見の行を予測できるか」という問いに答えるだけです。しかし、実運用ではより困難な問い、「モデルが未見のユーザーや患者、デバイスを予測できるか」が問われます。
同じユーザーに属する 5 行のデータがある場合、ランダム分割ではそれらが訓練セットと検証セットにばらばらに分けられてしまいます。その結果、モデルは「新しいユーザーへの一般化」ではなく、「既存のユーザーを認識した」という点で評価を得てしまいます。
60 人のユーザーを持ち、各ユーザーにほぼ同一の行が複数含まれる合成データでテストすると、train_test_split を用いた場合のスコアは 0.97 に達しますが、GroupShuffleSplit で各ユーザーのデータを片側に固定して分割した瞬間、スコアは 0.89 に低下します。この 8 ポイントの差こそが、モデルが単に記憶していたことを示す証拠です。
データをグループ単位で扱う場合は GroupKFold や GroupShuffleSplit を、時系列データの場合は TimeSeriesSplit を使用すべきです。なぜなら、ランダムな分割を行うと、未来のデータを学習に用いて過去を予測するという不自然な状況が生まれてしまうからです。
ラベルによる層別化はこのケースでは無意味であり、train_test_split にはグループを認識する機能自体がありません。
クロスバリデーションのガイド(https://scikit-learn.org/stable/modules/cross_validation.html)では、どのスプリッターがどの境界条件に適合するかを整理しています。いずれのスプリターを選ぶかを決める前に、モデルが何に対して一般化すべきかを明確にする必要があります。つまり、対象は特定のエンティティなのか、それとも時間軸上の一点なのかを定義することが重要です。
3. トレーニング時と推論時に異なる前処理コードを実行している
スケーリングのズレ(Skew)は、データリーケージに似ていますが、逆方向の問題を指します。リーケージが評価時に保持された行から情報を借用してしまうのに対し、スケーリングのズレは学習後に異なる変換パスを適用することで発生します。
この問題は最初は無害そうに見えます。例えば、ノートブックで特徴量をある方法でスケーリングし、推論用の関数では同じように手動で再実装しているケースです。しかし、その失敗は深刻な規模に達します。学習済みのスケーラーを再利用せず、5 行のバッチに対して再度スケーラーを学習させると、同じデータでも約 4 つ標準偏差分も値がずれてしまう可能性があります。これは「予測」と「コイン投げ」の違いを生むほど重大です。
似ているように見えるコードは、契約(保証)ではありません。推論時には、学習時に使用したパラメータ、特徴量の順序、データ型(dtype)、欠損値の処理ルールを完全に一致させる必要があります。最も安価で確実な対策は、学習済みのパイプラインオブジェクト自体を学習パスと推論パスの両方に含めて配布することです。
これを検証するには、1 つのテストデータセットを用意し、それを学習パスと推論パスの両方に通します。もし出力結果がどこかでも異なる場合(名前、順序、データ型、形状、値のいずれかで)、推論パスが何らかの点で誤りを示していることになります。

4. ライブラリにシードを設定しただけで実験の再現性を保証するのは誤り
スクリプトの冒頭で random.seed(42) を実行しても、得られるのは一時的な安心感に過ぎません。Python の標準ライブラリ random、NumPy、そして PyTorch はそれぞれ独立した乱数ジェネレータを備えており、最初の 1 つだけをシード化しても、残りの 2 つは設定されていない場合と同じ出力を生成し続けます。さらに、ワーカープロセスを持つ DataLoader を使用すると、独自のシードルールが追加されるため状況は複雑になります。
完全に決定論的なカーネルを使用するには、明示的にリクエストする必要がありますが、そのためにはパフォーマンスの低下を伴う場合があります。これは PyTorch の再現性に関するノート で明確に説明されています。同様のノートでは、シードの設定数に関わらず、PyTorch のバージョン間やプラットフォームの違い、CPU と GPU での実行環境の違いによって結果が同一になることは保証されないという現実も示しています。つまり、再現性の確保は単なるシード設定の問題ではなく、むしろ記録管理の問題なのです。
ランのシード値、データのスナップショット、コードのバージョン、設定内容、依存ライブラリのバージョンなどをログに記録しておけば、後から環境を再構築することは可能です。しかし、42 という数字 1 つだけあれば環境が再現できるわけではありません。この記録プロセスを自動化する実用的な方法として、Weights & Biases のクイックスタートガイド が紹介されています。
5. 評価状態と勾配無効化の混同に注意
model.eval() と torch.no_grad() は、どちらも検証ループ内で使われるため混同されがちですが、実際には異なる仕組みを制御しています。eval() モードは、ドロップアウトやバッチ正規化など訓練に敏感なモジュールを推論動作へ切り替えます。一方、no_grad はオートグラッドが計算履歴の記録を行わないようにするだけの機能です。
学習モードのまま no_grad コンテキスト内でドロップアウトモデルを同じ入力に対して 2 回実行すると、出力が異なります。これはドロップアウトがまだ機能しているためです。ある小規模なモデルでは、1 回目の出力が -0.1410、2 回目が 0.0071 という結果でした。一方、model.eval() に切り替えて同じ 2 回の呼び出しを行うと、両方とも全く同じ答えを返します。
依存関係は逆方向にも成立します。no_grad を設定しない評価モードのモデルでも、順伝播ごとに勾配が記録されてしまうからです。検証ループでは両方のスイッチを切り替える必要があり、処理後はモデルを学習モードに戻す必要があります。
model.eval()
with torch.no_grad():
val_loss = criterion(model(x_val), y_val)
model.train()PyTorch の autograd に関するドキュメントには境界条件の詳細な説明が記載されています。ブロック内で一度も勾配が必要ない場合、torch.inference_mode() は no_grad よりも厳格にその領域をロックします。
現在 Dropout が実装されていないモデルであっても、アーキテクチャが変更される可能性を考慮し、明示的に eval() を呼び出すべきです。この呼び出しにはコストがかかりません。
6. ブロードキャストによる誤ったテンソル形状の隠蔽
ブロードキャストは、損失関数に到達するまでは機能として有効ですが、そこでは問題を引き起こす可能性があります。例えば、予測値の形状が [batch, 1] で、ターゲットが [batch] の場合を想定してください。この状態で MSELoss を実行すると、引き算の結果がブロードキャストによってバッチごとの行列全体に展開されてしまいます。その結果、すべての予測値がすべてのラベルと比較されるという誤った計算が行われてしまうのです。
バッチサイズ 32 の場合、これは 32×32 のグリッドを意味しますが、損失は計算されます。その値は 1.63 です。一方、形状が正しいバージョンでは同じテンソルで 1.85 が得られます。1.63 という数値自体に不審な点はなく、例外も発生しません。ただし PyTorch はここで UserWarning を出力します。テストはこの警告をエラーとして扱うべきです。MSELoss のドキュメントでは、ターゲットは入力と同じ形状であることが明記されています。つまり、この修正は契約を一度定義し、境界でそれを強制するものです。
pred = model(x).squeeze(1) # [batch, 1] -> [batch], on purpose
assert pred.shape == target.shape
loss = criterion(pred, target)ブロードキャストがバグではないことは、ブロードキャストのセマンティクス が示す通りです。問題となるのは、損失関数や評価指標、ラベルの要件で厳密な一致が求められている境界面で、暗黙的な展開を許容してしまうことです。
7. 保存したモデルを不変でポータブルなファイルとして扱う
最後の境界線はファイルそのものです。pickle、joblib、あるいは cloudpickle によって作成された Pickle モデルは、単なる受動的なデータではありません。
1 つのファイルを読み込むだけで任意のコードを実行できてしまうため、悪意のある __reduce__ メソッドを含む 5 行分のファイルが問題となります。
信頼できないソースからのアーティファクトをロードすることは、絶対に避けるべきです。pickle.load 実行時に悪意のあるコードが実行されるリスクがあるため、検証されていないデータを読み込むことは危険です。
バージョンドリフトは劇的な問題を引き起こすわけではありませんが、同じようにワークフローを阻害します。その理由は、scikit-learn が異なるライブラリバージョンで保存されたモデルの読み込みをサポートしていないからです。すべてのアーティファクトには、トレーニングレシピ、データ参照、依存関係のバージョン、そして主張する検証スコアをセットにして提供するようにしましょう。
本番環境への展開前に、実際の運用環境で読み込み、固定されたテストデータを通じて完全な前処理から予測までのパスを走らせて検証する。フォーマットを変えても問題が単に場所を変えるだけで解消されるわけではないため、特定のフォーマットだけが安全策となるわけではない。
ワークフローの境界を実証する
これら7つのミスは、自らを告発してくることはありません。だからこそ、レビューは反応ではなく習慣として定着させる必要があります。以下の4つの問いが、その範囲をカバーします。
各ステップは何を学び、どの行から学んだのか?
提供時に生データを処理するコードは何か、そしてそれはトレーニングで使われた契約と一致しているか?
メトリクスに到達した状態と形状はどうだったか?
アーティファクトを読み込むのに信頼できる環境はどれか?
これらの問いにコードと記録されたメタデータから答えられるワークフローこそが、評価に値するものです。答えられないものは、有望な推測を抱えているに過ぎません。
Nahla Davies はソフトウェア開発者であり技術ライターです。技術ライティングに専念する前は、Samsung、Time Warner、Netflix、Sony などのクライアントを持つ Inc. 5,000 に選出された体験型ブランディング組織でリードプログラマーを務めるなど、数々の興味深い職務をこなしました。
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み