先日MLOpsの勉強会に出てました。
その場では、「ワークフローライブラリ使ってるとなんかイケてるんだなあ」くらいにしか思ってなかったんですが、機械学習の実験をしていて必要性を感じる場面があったので、試しに使ってみることにしました。 勉強会ではgokartを推してたんですけど、今回はgokartでラップされる元になってるluigiを使ってみたのでそのメモです。
機械学習ワークフロー
luigiについて考える前に、「そもそも機械学習のワークフローとは?」ってところから確認します。
機械学習ではざっくりと、データを読み込んで、前処理して、学習して、推論して、、、といった流れがあり、 こんな感じに処理に依存関係・前後関係があることが一般的です。
機械学習では、このような処理の流れを定義する必要があり、luigiを始めとする多くのワークフローライブラリではこのワークフローの記述・管理を簡単にします。
具体的に何が嬉しいの?
実務での機械学習アプリケーションは、データ量・計算量も多く、モデルは複雑になりがちで、その上データの傾向に合わせてモデルをメンテナンスし続ける必要があります。
そんな状況下で起こるシチュエーションとしては、
- コードが複雑になりすぎて何がどうなってるか解読しにくい
- モデルを作ったはいいが、再現性が取れない
- 処理に非常に長時間を要し、トライアンドエラーになりがちな機械学習プロジェクトの開発速度低下
などなど、機械学習ならではの難しさが出てしまいます。
それをなんとかして、
- コードのメンテナンス性の向上
- 再現性の確保
- 再実行の時間の削減
などを(他にもきっとメリットはあるとは思いますが)実現するのが、この手のライブラリを使用するメリットのようです。
luigi
luigiはSpotifyがOSSとして開発している、バッチジョブのパイプラインを構築するワークフローライブラリです。
公式のドキュメントはこちら。
名前の由来は、機械学習のワークフローを配管にたとえて、それを管理する「配管工」のキャラクターである某ゲームキャラクターが由来になっているようです。赤い方じゃないのは、単純にコーポレートカラー的に緑だったからでしょうか。 なんにせよ、洒落てますね!
説明資料
Spotifyの中の方の説明資料があるので、基本的なところはこちらをご参照ください。
書き方
luigiでは、処理の最小単位をTaskと呼ばれるClassで扱い、このTaskの中身を記述していくことでパイプライン全体を記述します。 各Taskには何を書くかというと、
- requires
- output
- run
の3種類のメソッドを記述します。
requiresでは依存Taskを戻り値として記述します。 前の処理がある場合には、依存関係のあるTaskのクラスを戻り値に指定します。 複数のTaskに処理に依存している場合には、戻り値を複数にします。
outputではTaskの出力オブジェクトを記述します。 出力オブジェクトは複数取ることもできますが、ドキュメントによればoutputオブジェクトは単一にするのが望ましいとのことです。
runではoutputを作るまでの処理を記述します。 requiresで記述したTaskのoutputオブジェクトを使用したい場合には、inputメソッドによって使用することができます。
以上のように記述することが強制されるので、誰が書いても同じようなコードになります。 これは、コードの保守性の観点から考えても非常に良いことだと思います。
一点注意すべきは、Task間のデータ受け渡しがファイル形式になりがちという点です。 大きな処理を作る際には、ファイルIOにかかる時間を考慮して設計する必要がありそうです。
Visualiser page
タスクの進行状況についてブラウザで可視化する機能もついています。
各タスクの依存関係のグラフも可視化できるようになっています。
タスクの関係や進行状況が直感的に確認できるのは良さそうです。
使ってみる
そんなこんなで使ってみます。
インストール
pip install luigi
簡単ですね。
Titanicでやってみる
kaggleのチュートリアルでおなじみのタイタニックの問題をluigiを使ってやってみます。 普通はnotebookで実験していくのは百も承知ですが、あえてluigiで書くとどんな感じになるかやってみます。
もとの実装
こんな感じでしょうか。 流れとしては以下のようにシンプルな形になっています。
- データの読み込み
- 前処理
- 欠損値の補完
- モデルの学習
たまたま手元にあったコードなんでちょっとアレですが、まあ動いてました。
luigiを使って書いてみる
それではluigiを使ってワークフローを作ってみます。
とりあえず素直に前処理と学習と推論に分けてみました。
動かしてみる
ブラウザで管理画面を使用する場合には、先にデーモンを起動しておきます。
luigid
実行するときにはこんな感じになります。
python titanic_workflow.py Predict --local-scheduler
ブラウザで処理状況の確認をする場合に"--local-scheduler"を外して実行します。
python titanic_workflow.py Predict
ワークフローを可視化する
http://localhost:8082にアクセスすると、上のような画面でフローの進捗や依存関係を可視化する事ができます。
その他の特徴として、なにかの原因で処理が途中で中断してしまっても、実行をやり直せばluigiが自動的に未完了の箇所から処理を再開してくれます。 これは、すでに前のTaskのoutputが指定のディレクトリに存在するかを判断しているようで、それらが存在していれば処理をスキップするようです。
参考にさせていただいた記事
こちらの記事を参考にさせていただきました。
感想
今回はluigiを使ってみました。 実感としては、notebookなどでプロトタイピングが終わった後、プロダクションに乗せて実運用する際には、たしかに使えると思いました。 処理が止まった際もどこまで完了しているか途中経過を確認できますし、途中から再実行することもできるので運用としても助かりそうです。 まだまだ使えてない機能もたくさんありますので、そのへんは使いながら慣れていきたいと思います。