[ML Ops] Pythonでマルチプロジェクトをやりたい


この記事は ぷりぷりあぷりけーしょんず Advent Calendar 2020 の13日目の記事です。

はじめに

普段は Python を使って MLOps の業務に取り組んでいます。元々は、 Java や Kotlin など、 Maven や Gradle といったビルドシステムで開発を行っていました。
最近使っている Python は単発のスクリプトや機械学習の文脈では非常に強力なツールだと思います。しかし、実際にプロダクトレベルの運用を始めると、依存関係や共通化の難しさ、ビルド周りの弱さに疲弊してしまう側面もあると思います。
そこで、Java や Kotlin などで使われるマルチプロジェクトの構成を Python で再現したら非常に開発が楽になりました。
今回はそのナレッジを記事にしていきます。

※今回の記事で紹介するアイデアはこちらを参考にさせていただきました → https://medium.com/opendoor-labs/our-python-monorepo-d34028f2b6fa

なぜマルチプロジェクトが必要だったのか

冒頭に記述した通り、筆者は普段は AI に関係するプロダクトで MLOps を中心とした業務に取り組んでいます。
開発を進めていくうちに以下のような課題が発生しました。

  • 学習時と推論時に同じ前処理をしなくてはいけない → コードのコピペが発生
  • モノレポにしたいけど、 pip の依存解決のトラブルが多くてできない → 増える新しいモジュールやリポジトリ

これらの課題を整理した結果以下のような用件が出てきました。

  • それぞれのモジュールは独立した依存関係を定義したい
  • 共通化した処理を使いまわしたい
  • 依存先の一覧などが一眼でわかるようにしたい

これらを解決するには、 private な pip server を立てたり、 Pickle Object を Storage へ置くなどの案があがりましたが、いずれもメンテナンスコストの観点から却下しました。そして、たどり着いたのが、 Maven や Gradle などを使ったマルチプロジェクトの構成を Python & Poetry で再現することでした

マルチプロジェクトの利点

実際の Maven, Gradle などによるマルチプロジェクトの概要や利点は、まとまった物が世の中にたくさんあるので、解説しません。
今回は ML Ops と Python の都合によるメリットを上げていきます。

  • 前処理が共通化できる
  • モデルごとに依存を個別管理できる

また、マルチプロジェクトを実現するために、 Poetry を利用しています。すると、追加で以下のようなメリットも享受できます。

  • setup.py や requirements.txt を用意しなくて良い。
  • コマンド1つで仮想環境と依存が解決できる。
  • pipenv よりも洗練された 依存解決のアルゴリズムを利用できる。

さあやってみよう

ディレクトリの構成

ディレクトリ構成は非常にシンプルです。

.
├── README.md
├── libs
│   ├── lib-one
│   └── logger
└── projects
    ├── __init__.py
    ├── project-one
    └── project-two

それぞれは以下のような役割になっています。

./lib -> 複数のプロジェクトで使いまわしたい共通モジュール
./projects -> ユースケースを実行するメインモジュール

projects 以下のモジュールは libs 以下のモジュールに依存します。

モジュールを追加する

モジュールを追加する時は libs でも projects であっても同じです。
poetry の new コマンドで生成していきます。


# 共通モジュールを追加する場合
cd libs/
poetry new lib-two

# メインモジュールを追加する場合
cd projects/
poetry new project-three

以上でOKです。Poetryが勝手に雛形を生成してくれます。
あとは好きなように実装をしていきましょう!

モジュールに依存関係を追加する

それでは、実際にメインモジュールに共通モジュールの依存関係を追加する方法を見ていきます。と言っても、とても簡単です。


cd projects/project-one
poetry add ../../libs/lib-one

poetry add {入れたい共通モジュールのパス} だけでできてしまいます。
pyproject.toml を見てみるときちんと依存が追加されていることが確認できます。


[tool.poetry]
name = "project-one"
version = "0.1.0"
description = ""
authors = ["ya-mori <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.7"
lib-one = {path = "../../libs/lib-one"}

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

この依存関係の追加方法は公式のドキュメントにも記載があります。(https://python-poetry.org/docs/cli/#add)

この方法の素晴らしい点は、共通モジュールの変更がすぐに反映されることです。つまり、いちいちビルドし直す必要はありません。

以上で、 好きなメインモジュールに好きな共有モジュールの依存を追加することができました

さいごに

今回は Python でマルチプロジェクトを実装する方法についてまとめて行きました。
実際の業務ではこれらを Dockerfile に修めなくてはいけないため、もう一苦労あったのですが、それはまた別の機会にご紹介したいと思います。
少しでもあなたの役に立てれば幸いです。