💬

機械学習モデル運用で『できると思ってた前提』が崩れた話

に公開

目次

初めに

機械学習モデルの運用においては、一般に「冪等性」が確保されていることが望ましいとされています。
同じ入力に対して同じ出力が得られることは、再実行性・検証可能性・障害復旧の観点で重要な性質です。

冪等性とは、同じ操作を何回繰り返しても、最終的な状態や結果が変わらない性質を指します。

私自身も、推論パイプラインにおいて冪等性を担保した設計を目指していました。
しかし実際に運用を進める中で、その難易度を大きく誤認していたことに気づかされました。

本記事では、

  • 推論結果の冪等性確保に関する誤認
  • S3 / データレイク設計におけるスキーマ管理の誤解

という2つの失敗を題材に、
「どこまでが技術的に制御可能で、どこからが設計上割り切るべき限界なのか」
について、当時の思考と学びを整理します。

同じように「正しそうな前提」を無意識に置いたまま設計を進めてしまうケースは、
機械学習モデル運用やデータ基盤設計において決して珍しくありません。

本記事が、そうした前提を見直す一助になれば幸いです。

学び

  • 個別技術そのものよりも、「どこまで制御できるのか」という裏側の前提理解が、設計の成否を大きく左右すること
  • 冪等性や再現性は自動的に得られる性質ではなく、計算モデル・並列性・リソース制約を含めて設計しない限り成立しないこと
  • 制約が明確な環境ほど設計判断の質が問われるため、環境選びだけでなく「自分で制約を定義する視点」が重要であること

異なるスキーマをパーティションカラムで制御できると勘違いしていた話

まずはS3における特徴量ストア設計での失敗の話から。

  • 前提の共有
  • 失敗した内容
  • 失敗した理由
  • 改善策

の順番で書きます。

前提(やりたかったこと)

運用しているモデルは、複数のメディア(約48種)を同一の枠組みで学習するタイプです。
ベースとなる特徴量設計は近いものの、メディアごとに利用できるログや属性が異なるため、実際にはメディア別に特徴量の列構成(スキーマ)が微妙に異なります

今回の取り組みでは、学習・検証に用いた特徴量データをS3へバックアップしておき、

  • 失敗した実験だけを再学習する
  • 検証対象のみを部分抽出して再現する
    といった運用を、低コストに回したいと考えていました。

失敗した内容

当初はパーティションカラムに

  • media_id(メディア識別子)
  • yyyymm(年月)
    を持たせれば、
  1. 目的のデータだけを絞り込めてスキャン量を削減できる(=性能/コスト改善)
  2. パーティションが分かれているので、メディア間で特徴量が混ざらない
  3. よって、メディアごとに列が違っても「同居できる」と期待した

という発想で設計しました。
エクスプローラー上におけるファイル管理のように、yyyymmまでは同じパスで、それ以下のフォルダがメディアごとに分かれてデータがおかれるイメージでした。

設計当初のイメージ図

しかし実運用では、2個目のメディアの特徴量データをバックアップしようとしたタイミングで、
「スキーマが一致しない」旨のエラーにより破綻しました。

失敗した理由:スキーマ定義の仕様

S3自体はスキーマレスで、CSV/Parquetなどのファイルは自由に置けます。
ただし、Athena等でクエリ可能にするには Glue Data Catalog(カタログ)でテーブル定義(スキーマ)を持つ必要があります。

重要なのはここで、

  • パーティションを切っても「テーブルは1つ」
  • カタログに登録できるスキーマ(列定義)は原則1つ

という点です。

つまり、S3上にメディア別のフォルダ(=パーティション)を切っていても、
Athena/Glueから見れば「1つのテーブルに対して1つのスキーマ」で扱われるため、
メディアごとに列構成が違うデータを混在させると整合が取れず、エラーになります。

イメージは下記。

実際の保存イメージ図

物理的な切り分けはイメージ通りエクスプローラーのように分けられていたのですが、論理的には1つのテーブルとして扱われ、1テーブルにつき1スキーマという仕様に引っかかった、というわけです。

回避策としては「全メディアの列の和集合」をテーブルスキーマとして持ち、
存在しない列を NULL で埋める設計も可能です。
ただしこの方式は、今回の用途(バックアップ→即学習の再現)に対して

  • 不要列が増え、I/Oやスキャンが増えやすい(コスト/性能悪化)
  • 前処理の条件分岐が増え、学習の即時性が落ちる
  • 「メディアごとに正しい特徴量だけを素直に使う」運用から遠ざかる

という理由で採用しませんでした。

改善策:別テーブルとして保存

結論として、メディアごとにテーブルを分離して保存する方針に変更しました。

具体的には、メディア単位でデータセット(=テーブル)を分けることで、

  • テーブル定義(スキーマ)と実データの整合が取れる
  • 「そのメディアで有効な特徴量だけ」を素直に扱える
  • バックアップから再学習までの導線が単純になる

という状態に落とし込みました。

物理的にテーブルを分けることによって異なるスキーマを定義できるようにする、というしごく当たり前な解決策ですね。

最終的なバックアップ取得イメージ図

S3は保存しているデータ量にコストがかかるため、テーブルを分けても1つのテーブルで保存しても料金は変わらないようです。

推論結果の冪等性確保の難易度を勘違いしていた話

次に推論結果の冪等性を確保しようとして失敗した話。

  • 前提
  • 冪等性が必要な理由
  • 冪等性確保の難易度を見積もりを誤っていた話
  • 原因と対応

の順番で書きます。

前提

今までは推論結果の正常性確認を手動実行していました。
理由は今月の結果と先月の結果を使用して正常性確認をしており、そのデータが実行環境と別に存在していたから。

下記図の右側である「分析基盤」のみにデータがあった、ということですね。

処理全体を表すアーキテクチャ図

ただ、手動実行だとヒューマンエラーが発生したり、修正が入った時のテストが自動では行われないためバグが潜みやすいという課題がありました。

そこで、正常性確認を今月データだけで完結するようにしたうえで、バッチ処理に載せて全自動化しよう、という取り組みが走っていました。

なぜ冪等性確保が必要なのか

上記取り組みを完了し、正常性確認が正しく動くかを確認するべくテストデバッグを実行したところ、手動実行した時の結果とずれが生まれていました。

最初は追加ロジックにミスがあるのかなと考えて調査を進めていきましたが、そもそも推論結果自体が毎回微妙に異なっているという事実に気づきました。

つまり、正常性確認が壊れていたのではなく、比較対象となる推論結果が冪等ではなかった、という状況でした。

この状態ではロジックがあっているのかどうかを100%正確に確認するのは難しかったため、外部にロジックの正当性を説明するために冪等性確保が必要になったというわけです。

冪等性確保の難易度誤認

確認すると下記3点を修正すれば行けそうでした。

  • ハイパラ調整時のシード値追加
  • テスト分割時のシード値追加
  • 特徴量データ取得時のORDER BY追加

モデル作成時のシード値はあるものの、ハイパラ調整とそれに使用するデータが毎回ランダムになっていたため、推論結果が異なっていた、という状況です。

ただ、上記を流してみると下記のような結果になりました。

  • 推論結果が手動実行と異なる
  • 選択されるハイパラが手動実行と異なることがある
  • 異なるハイパラが選択されたメディアを見ると、途中までハイパラ探索の仕方が全く同じ

...???
データの取り方も分割の仕方もハイパラ調整の仕方も同じなのに結果が異なるのはなんでだろうか?

しかも選ばれるハイパラが同じメディアもちらほら存在している。
異なるハイパラになったとしても途中までハイパラ探索の仕方が同じになっている。

ハイパラ探索の途中で何かしらのブレがあるんだろうなぁ。と思いつつ、何が悪いのか分からなかったのでGemini先生に聞いてみました。

実装は完ぺきに冪等性が確保されるものになっているのに`max_depth`が大きくずれてしまうのは非常に気持ちが悪いですよね。

原因は「LightGBMの並列計算によるごく極小なスコア誤差」が「Optunaの次の探索ルート」を大きく変えてしまった(バタフライ効果)ことにあります。

...???

原因:複数CPUを使用した並列処理による出力順のずれ

上記LightGBMとは、勾配ブースティング決定木(GBDT)を高速・大規模データ向けに実装した機械学習ライブラリのことで、精度が高く、学習が速く、運用で扱いやすい表形式データ向けモデルです。

Optunaとは機械学習モデルのハイパーパラメータ最適化を自動化するための Python ライブラリのことで、ハイパーパラメータ探索を、効率よく・賢く回すための仕組みを提供してくれます。

ただ、並列処理は実装していなかったので、ハルシネーションでもしているのかと冷笑しつつ深ぼってみたところ、
「LightGBMそのもの」を並列処理しているといっているのではなく、
「学習フェーズにおいて複数CPUを使用していること」を並列処理と言っているようでした。

複数CPUを使うと何が起きるか

複数CPUを用いた並列計算では、以下のようなことが起こります。

  • 各CPUコアが部分的な計算を同時に進める
  • 計算結果は最後に集約されるが、その集約順序は固定されていない
  • 浮動小数点演算では、加算や平均の順序が変わると、ごく小さな数値誤差が生じる

この「ごく小さな誤差」は一見すると無視できるように見えますが、
Optunaでは探索パラメータを変動させる大きな要素になります。

Optunaは動的にハイパーパラメータを変更させながら複数回探索を行い、その中から最も推論結果が高くなるハイパラを提示するアルゴリズムです。
複数回探索を行う際、前の探索の結果を受けて次の探索パラメータを決めるため、たとえ0.00000001だけ結果がずれていると、次の探索パラメータが異なるものになるのです。

その結果、

  • シード値等は固定されているため探索の途中までは完全にパラメータが一致している
  • CPUの計算順によってあるタイミングから微妙な差分が生じる
  • 最終的なハイパーパラメータが異なるため推論結果が一致しなくなる

という挙動が発生していた、というわけです。

対応策と妥協案

単一CPUによるハイパラ探索

上記に対応するためには、1つのCPUでハイパラ探索をすると解決します。
ただ、今のバッチ処理では実に96個ものCPUを使用して並列処理しているため、最低でも数十倍処理時間が延びる計算になり、せっかくSHAP値処理を最適化して削ったコンピュートコストが膨れ上がってしまいます。
SHAP値の話は下記。
https://zenn.dev/zenn_mita/articles/bd54d81c4dab31

少し計算すると、今のハイパラ探索は1メディア当たり5分前後。
それが10倍になると50分で、それが48メディア存在するため、1時間当たりのコンピュートコストを$6.05とすると、1実行当たりの追加コストは下記のようになります。

50[m] * 48[メディア] ÷ 60 [m] * 6.05[$] * 150[円/$] = 36300[円]

実に40時間の延長。コスト的にも時間的にも許容できるレベルではありません。

複数メディア学習の並列実行

上記を受けて、実行時間を短縮するために、学習の並列実行を考えました。

ハイパラ探索を単一CPUで実行する代わり、複数メディア一気に学習するようにすれば、単純計算で実行時間が1/48になります。

今の逐次学習・逐次推論に対して、いつか並列処理を入れたいと考えていたこともありこの案で進めようと考えていました。

実現のための設計を考えているとき、今まで並列処理を加えられなかった理由を思い出しました。
それが「メモリの使用量」です。

現在の逐次学習で使用しているメモリは大体200GB未満。
今の実行環境でOut of Memoryが発生するのが200GB以上。
...とてもかつかつなのです。

1メディア当たり1億レコード以上をメモリに広げなければならず、1メディア広げるだけで200GBぎりぎりになっているため、2つ以上のメディアを並列処理しようとすると即座にOOMが発生してしまうんですよね。

このため、メディアごとの学習を安易に並列化すると、現実的にはOOMリスクが極めて高く、結果として本番運用で採用できる見込みは低いと判断しました。

結果

本題である推論結果の冪等性確保について、最終的にはビット単位で完全に一致する厳密な冪等性を保証することは諦めるという判断を取りました。

これまで検討してきた通り、 並列計算や最適化アルゴリズム(Optuna)の性質、
さらに実行時間・メモリ使用量といったアーキテクチャ上の制約を踏まえると、
厳密な冪等性を追求し続けること自体が現実的ではないと判断したためです。

一方で、「再現性を放棄する」選択をしたわけではありません。
最終的には、同一条件で複数回推論を実行し、その平均値を用いて結果を評価・確認する
という方針を採用しました。

この方法であれば、

  • 単発の実行結果に依存しすぎない
  • 微小な数値揺らぎの影響を抑えられる
  • 実行時間や計算資源の増加を最小限に抑えられる

といった点で、今回のユースケースにおいて十分に実用的な再現性を確保できるかな、と考えています。

感想

今回は初めてアーキテクチャの制約によってやりたいことがそのままできないケースにあたりました。

個人的には、こうした制約の中で現実的な落とし所を探し、
要件や設計を組み替えていく過程こそが、技術力が問われる部分だと感じています。
まだまだ判断の精度や引き出しは足りないと感じる一方で、
「制約を前提に考える」という視点を持てたこと自体は大きな学びでした。

また、強い制約が自然に存在する環境は意外と限られているとも感じています。
多くのユーザーに使われているサービスや、大規模データを扱うシステム、
SLAが厳しい現場などでは外部要因として制約が発生しますが、
そうでない場合は、意識的に制約を設けない限り、
設計判断の難しさに直面しづらいとも思いました。

今後は、与えられた制約をどう扱うかだけでなく、
「どこに制約を置くべきか」を自分で定義することも含めて、
技術的な意思決定の精度を高めていきたいと考えています。

そもそも最初から制約のある環境に行けたら一番早いのですが...
そういった環境を選べるよう、今いる環境の中で工夫していきます。

Discussion