エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

MLOpsの「あるある」課題の解決と、そのためのライブラリgokart

こちらはエムスリー Advent Calendar 2024 2日目の記事です。

こんにちは、AI・機械学習チームの池嶋(@mski_iksm)です。

近年、機械学習は多くのアプリケーションで当たり前のように使われるツールになりつつあります。ですが機械学習は、ライブラリを呼び出すだけで簡単に使える、というわけにはいかない特有の難しさもありますよね。 例えば、モデルの学習実験を試行錯誤しながら何度も繰り返しているうちに、「どのデータを使い、どんな設定で学習させたモデルが一番良かったのか分からなくなった」という経験はないでしょうか。 また、本番環境で使用するモデルが実験環境で作ったものを再現できず、「実験ではうまくいったのに、本番ではイマイチ…」といった問題に直面したことがある方も多いかと思います。

こうした課題に取り組みながら機械学習プロジェクトの生産性を向上させるため、近年ではMLOpsの取り組みが広まっています。 私たちエムスリーのAI・機械学習チームも例外ではなく、日々さまざまな改善活動を行っています。

この記事では、私たちのチームが直面しているMLOpsにおける「あるある」な課題と、それらを解決するための取り組みについてご紹介します。 また、チームでは改善ノウハウを共有しやすくするために、独自のパイプラインツールである gokart を開発・活用しています。この記事では、このgokartをどのように使って課題を解決しているかについてもお伝えします。

なお、本記事はPyCon JP 2024で使用した資料を基に構成しています。

speakerdeck.com

tl;dr

  • エムスリーでは、MLOpsにおける「あるある」な課題を解消するため、結果の保存、処理のパイプライン化、そしてコードの標準化・共通化に取り組んでいます
  • さらに、これらの解決策をチーム全体で横展開しやすくするために、独自のパイプラインツール gokart を開発・活用しています

gokartとは?

gokart は、データ処理の流れを定義・実行するためのパイプラインツールです。 ユーザーは、データのダウンロード、特徴量の作成、モデルの学習といった各処理を「タスク」と呼ばれるクラスとして記述し、それらを連結させることでパイプラインを定義します。 実行時には、gokartがタスク間の依存関係を自動で解決しながら、処理をスムーズに進めてくれます。

github.com

gokartは、Spotify社が開発したツール luigi をベースにしたラッパーとして開発されました。 機械学習における「あるある」な課題を解決するため、再現性の確保や処理の効率化などのノウハウが機能として組み込まれています。 現在も、エムスリーのAI・機械学習チームのメンバーを中心に、OSSとして活発に開発が進められています。

AI・機械学習チームでは、ほぼすべてのPythonプロダクトにおいてgokartを採用しています。これにより、プロダクト間でノウハウを容易に共有できる環境が整っています。

本記事では、機械学習における各課題がgokartを使ってどのように解決されているかを具体的にご紹介していきます。

課題1: 実験した機械学習モデルの再現性がない

機械学習モデルを使ったプロダクトを開発する際、まず実験環境でモデル作成を試行錯誤し、ある程度良いモデルができたら本番環境にデプロイする、という流れが一般的かと思います。 このとき、再現性の観点から、実験環境と本番環境でモデルが完全に一致することが非常に重要です。

しかし、モデルが一致しなくなる要因はさまざまあります。 たとえば、モデルのハイパーパラメータを保存し忘れた場合、モデルの複雑度が異なる全く別のモデルが作られてしまうことがあります。 また、ランダムシードを保存しなかった場合でも、学習過程が変わるため、再現ができなくなってしまいます。 さらに、実験環境でモデルを作成してから本番環境に移す間にデータが更新されていると、新しいデータを用いて再学習したモデルが微妙に異なってしまう可能性があります。

こうした理由から、実験環境でうまくいったモデルを本番環境で再現しようとしても、期待通りに動作しないことがあるのは困りものです。

解決1: 全実験結果を設定とペアで保存

実験環境と本番環境でのモデルのズレは、処理の結果を保存し共有することで防ぐことができます。 例えば、特徴量データの作成など各処理が終わったタイミングで結果をファイルとして保存し、それを実験環境と本番環境で共有すれば、再現性を保つことが可能です。 手元のPCで作成したモデルを本番環境にアップロードしてデプロイした経験がある方も多いかと思いますが、再現性を担保するためにはこうした工夫が重要です。

gokart では、各処理ごとに結果を保存する仕組みが組み込まれています。 gokartでは、ひとまとまりの処理を「タスク(Task)」と呼ばれるクラスで定義し、複数のタスク間を依存関係で結びつけることで処理の流れを定義します。 各タスクは、自分の処理が完了すると、その結果をファイルに保存します。 この保存先ストレージを実験環境と本番環境で共有することで、完全な再現性を実現する仕組みになっています。

課題2: モデルのバージョン管理が面倒

解決1にて、モデルやデータを処理ごとに保存する仕組みを導入しましたが、それにより保存するファイル数が増えすぎて管理が煩雑になるという新たな課題が発生しました。 というのも、各処理ごとに結果ファイルが保存されるため、設定ファイル、特徴量データ、モデル、予測結果といった各ファイルの組み合わせを管理する必要が生じたからです。

例えば、どの実験で作成されたデータなのかを把握するために、ファイル名に実験番号を含める工夫は考えられます。 しかし、「この実験では特徴量データだけを作り直したい」といったケースでは、すべてのファイル名を統一することが難しくなる可能性があります。

「性能の良いモデルはできたけれど、どの設定やデータの組み合わせで生成されたのか分からなくなった」といった事態を防ぐためにも、ファイルや結果の適切な管理が欠かせません。こうした管理を効率的に行う仕組みが求められています。

解決2: モデルの作り方をパイプラインとして管理

ここでの対策は、モデルや特徴量ファイルそのものを直接管理するのではなく、モデルの作り方そのものを管理するという方法です。 どのデータソースを使用し、どのように特徴量を作成し、どのようにモデルを学習するか、といった一連のプロセスをすべてコードに記述します。 このコードを管理することで、モデルや特徴量ファイルの組み合わせを一元的に保つことが可能になります。

gokart は、まさにこの「モデルの作り方」をパイプラインとして定義するためのツールです。 gokartでは、処理を1つの「タスク(Task)」としてクラスで記述し、それらを連結させてパイプラインを構築します。 各タスクの結果がどのファイルに格納されているかはgokartが自動で管理してくれるため、ユーザーがデータやモデルのファイル名の組み合わせを覚える必要がありません。

さらに、gokartではパイプラインのコードと設定を別々のファイルとして管理できます。 機械学習では、一部のパラメータだけが異なるモデルを何度も試行することがよくあります。 このような場合、毎回コードを一から書き直すのは非効率です。 gokartでは、パイプラインのコードと設定を分けて管理することで、コードの再利用性を向上させ、効率的に実験を進められるようにしています。

課題3: 特徴量作成をモデルごとにやり直すのが面倒

機械学習モデルを作成する際には、モデルの種類やハイパーパラメータを変えながら何度も実験を繰り返すことがよくあります。 しかし、解決策2で述べたように、モデルの作り方をパイプラインとして定義している場合、設定を変更して実験するたびに、パイプライン全体を最初から実行し直すことになってしまいます。

具体的には、データのダウンロードや特徴量の作成といった処理も、毎回やり直す必要があるため、全く同じ処理を何度も繰り返すことになります。 これでは、実験ごとに時間やリソースを無駄に消費してしまい、非常に非効率です。

解決3: 必要な部分だけを逆算して再実行

この問題は、再実行が必要な部分を特定し、その部分だけを再実行することで解決できます。 たとえば、モデルの学習だけをやり直す場合、前段のデータダウンロードや特徴量作成はやり直す必要がないため、それらをスキップし、モデル学習の部分だけを再実行すれば効率的です。

しかし、このような部分的な再実行は簡単に実現できるでしょうか? モデル学習の再実行は処理のステップが明確に分離されているため比較的容易に行えますが、一部の特徴量作成をやり直す場合、その変更が影響を及ぼす部分だけを切り出して再実行する必要があり、難易度が高くなります。

gokart には、この課題を解決するための仕組みが備わっています。 再実行が必要なタスクを自動で判定し、そのタスクと影響を受ける下流のタスクだけを再実行する機能があります。 たとえば、一部の特徴量作成タスクだけをやり直す場合、そのタスクとその下流にあるモデル学習タスクなどの依存タスクだけを再実行し、それ以外のタスクはスキップする仕組みです。

この再実行機能は、gokartタスクの処理結果ファイルが既に存在するかどうかで判断されます。 gokartでは、タスクを実行する際に、まず対応する結果ファイルがストレージに存在するかを確認します。結果ファイルが見つかった場合、タスクの処理をスキップし、保存済みの結果ファイルを読み込んで次の処理に進みます。タスク本体の処理が実行されるのは、結果ファイルが存在しない場合、つまり初回実行時やパラメータ変更時だけです。

タスク結果ファイルの名前は、タスクのパラメータによって動的に決定されます。 たとえば、特徴量作成タスクの場合、データのソースや特徴量の作り方がパラメータとして指定され、これに基づいてファイル名が生成されます。パラメータが変われば新しいファイル名が生成され、既存の結果ファイルがないため、再実行が必要であると判断されます。

さらに、gokartではタスクのパラメータだけでなく、依存する上流タスクやその設定も自動的に結果ファイル名に影響を与える仕組みになっています。 たとえば、上図のようにモデル学習タスクの上流に特徴量作成タスクがある場合、特徴量作成タスクの設定パラメータが変更されると、それに依存するモデル学習タスクのファイル名も変わります。この仕組みによって、特徴量作成タスクの変更が下流のモデル学習タスクに影響を与えることをgokartが認識し、必要なタスクだけを再実行できるようになります。

課題4: 開発者によって書き方にばらつきがあり、担当外プロダクトの処理の流れがわからない

チームで開発を進めていると、開発者によってコードの書き方にばらつきが出てくることがあります。 エムスリーのAI・機械学習チームでは、プロダクトごとにコードリポジトリを分けるマイクロサービス形式で開発しています。この方式には、新しい技術を取り込みやすくなったり、コードの責任範囲が明確になったりといったメリットがあります。

しかし一方で、担当者によって書き方にばらつきが出るというデメリットも存在します。 たとえば、「どのスクリプトをどの順番で実行すればいいのか」「エンドポイントのファイルがどこにあるのか」といった基本的な構造が分かりにくかったり、「クラスの粒度」や「1行の長さ」といった細かい書き方の違いが目立つことがあります。その結果、コードが書いた本人以外には把握しづらいものになってしまいます。

この問題は特に、担当者以外のメンバーにも確認してもらう必要があるコードレビューの場で顕著になります。コードの内容を理解するのに時間がかかり、効率的なレビューが難しくなることも少なくありません。

解決4: 書き方の標準化を強制する

エムスリーのAI・機械学習チームでは、コードの書き方を標準化することで、書き方のばらつきを抑制し、チーム全体での開発効率を向上させています。

どのスクリプトを順に実行すればよいのか?

gokart を利用し、1つのパイプライン内でタスクを連結させる仕組みを採用しています。 これにより、特徴量作成やモデル学習といった処理ごとにスクリプトを分け、それらを順番に実行する必要がなくなります。 さらに、全処理を1つのパイプラインに集約することで、全体の流れが見通しやすくなり、可読性と保守性が向上します。

どこにエンドポイントのファイルがあるのか?

cookiecutter を使ってプロジェクトのテンプレートを定義しています。 cookiecutterは、プロジェクトのテンプレートを基に統一されたディレクトリ構造を自動生成するツールです。これにより、エンドポイントのファイルやその他の重要なファイルの場所が明確になり、新規メンバーでもプロジェクト構造をすぐに把握できるようになります。

ただし、cookiecutterでテンプレートを適用するのはリポジトリ作成時のみです。そのため、テンプレートに変更が加えられた場合、それを既存のプロジェクトに反映できません。

この課題を解決するため、チームでは cruft を併用しています。 cruftは、現在のプロジェクトと最新のcookiecutterテンプレートとの間の差分を検出し、それを既存のリポジトリに反映するツールです。これにより、テンプレートの更新内容を常にプロジェクトに適用できる仕組みが整っています。

クラスの粒度や1行の長さなど、細かい書き方の違いが気になる

AI・機械学習チームでは、「細かい書き方のルールはLinterに任せよう」という考え方を採用しています。

2024年11月現在、コードのリント/フォーマットツールとして ruff を使用しています。 ruffは非常に高速で動作するため、開発作業の妨げにならず、自動的にコードフォーマットを整えてくれます。これにより、コードスタイルが統一され、レビューや保守の負担が大幅に軽減されています。

また、gokartの書き方にばらつきが出る問題に対しても、独自ルールを設定したLinterを活用して対応しています。これにより、チーム内での統一的な書き方が維持され、開発効率が向上しています。

詳しい内容については、以下のブログ記事をご覧ください。

www.m3tech.blog

課題5: 特徴量作成・モデル学習を並列実行したい

データのダウンロードや特徴量作成は、複数のタスクに分割できることがよくあります。 たとえば、データダウンロードを月単位で行ったり、特徴量作成をカテゴリごとに分けて処理したりする場合です。これらの処理は互いに依存関係がないことが多いため、並列実行が可能なケースがあります。

しかし、それにもかかわらず、処理を直列に実行していると、不要な待ち時間が発生したり、リソースが効率的に使われない問題が生じます。 並列化できる処理を直列で進めることで、全体の処理時間が長引き、プロジェクトの進行に悪影響を与えることも少なくありません。

解決5: 分散処理を行う

依存関係のない独立したタスクを複数のノードに分散させることで、処理時間を大幅に短縮できる場合があります。 ただし、この方法を実現するためには、どの処理をどのノードに割り振るのかを適切に決定する必要があります。

gokart は分散処理をサポートしており、これを簡単に実現する仕組みが備わっています。 gokartで分散処理を行う際は、複数のノードに同一のパイプラインを投入するだけで実現可能です。gokartには、タスクの実行順序をランダムにする機能がデフォルトで備わっており、同一の優先度を持つタスクがある場合、下図のようにランダムな順番でスケジュールされます。

さらに、解決策3 で紹介したように、gokartには一度完了したタスクを再実行しない機能があります。このため、各ノードがランダムな順序でタスクを実行しても、すでに他のノードで完了したタスクはスキップされます。

これらの仕組み、すなわち「タスクのランダムな実行順序」と「再実行の抑制」を組み合わせることで、gokartは効率的な分散処理を実現しています。

We are hiring!

機械学習の開発には多くの試行錯誤が伴いますが、実験管理が混乱しやすいといった課題がつきものです。 安定した本番運用を実現するためには、管理コストを抑えつつ、再現性・再実行性・標準化を徹底する必要があります。 これらの課題を解決するノウハウを詰め込んだライブラリとして、私たちは gokart を開発しています。

エムスリーでは、このgokartを活用し、100を超えるマイクロサービスにノウハウを横展開しています。 gokartを通じて、機械学習プロジェクトの効率化と品質向上を実現してきました。

現在、エムスリーのAI・機械学習チームでは、こうしたMLOpsの改善に興味のある方を積極的に採用中です。 興味をお持ちの方は、ぜひ次のリンクからご応募ください。皆さんと一緒にMLOpsの課題を解決する未来を作れることを楽しみにしています!

jobs.m3.com