(異常に脱字が多かったので見直しました 12/28)
最近検証を兼ねて、Kedroを使ってMLモデル開発におけるワークフロー管理をしています。
Kedroはワークフロー管理ツールの一つであり、python関数をnode
というクラスでラップしてつなぎ合わせることでPipeline
を構成すること、node
の入出力は全てData Catalogと呼ばれるメモリやストレージを抽象化したファイルシステムクラスで管理されることが特徴です。io自体は別個のDataSet
というモジュールを用意して対応しています。
kedroの利用にあたって学習用に生成した特徴量と推論用に生成した特徴量を、中間生成物を含めて丸ごと別々に保存したいと考えましたが、どうもその分けて保存する方法についてストレートな解決方法が見つからなかったため、今回試行した方法をこちらに残します。
前提
kedroが自動生成してくれるプロジェクトテンプレートに従ってファイル構成を切ったとします。
kedroのバージョンは0.17.0です。
Data Catalogでのデータ管理
Data Catalogの用意の仕方はpython code上で定義する方法とyamlで記述し、hooksを仕掛けてkedro実行時にそのyamlをベースに生成する方法があります。今回は後者に基づくので後者の例だけ記述します。
Kedroは実行時に自動的にルートディレクトリからconf/*/catalog*
という名前のyamlを見つけて読みに行きます。
yamlに書かれた設定を元に、Data Catalogが作られます。
titanic_train:
type: pandas.CSVDataSet
filepath: s3://competitions/titanic/train.csv
credentials: minio
outputs:
type: pandas.CSVDataSet
filepath: s3://competitions/titanic/outputs.csv
credentials: minio
versioned: true
現行のkedroのバージョニング
DataSetのバージョニング
DataSetにはバージョニングの方法が提供されています。
AbstractVersionedDataSetというクラスを継承してDataSetクラスを実装し、かつcatalog.yml
においてversioned
をtrue
にすることでバージョニングが有効になります。
ただ困ったことに、この方法ではtimestampしかバージョン名に使えません。(おそらく)
コードでDataCatalog
を設定する場合にはこの限りではないのですが、少なくともyamlの記述からバージョンのフォーマットは変更できないようです。
さらにCLIを使って実行する場合はコードで生成したDataCatalog
を注入する手段は現状ないようです。
-> HOOKSにおいて直接言語で定義したDataCatalogインスタンスを呼び出せば良いようです。
時間以外でバージョニングする動機
私は生成した特徴量を時間以外のバージョン名でバージョニングしたい(trainとtestのように分けたい)と思っています。
これのために取れる手段としてはcatalog.yml
に新しくtest用のデータセットを記述し、さらにpipeline
も別途test用の物を生成する必要があります。
pipeline
定義時にはnode
が定義されている必要があり、node
の定義時にバージョンを読み取って参照するデータセットを変更する処理が必要になります。読めないコードの始まりです。
一方で当然ながら特徴量生成のロジック自体は学習時と推論時で同じである必要があるし、コード上でも共通化したいです。
これの回避を目的としてデータを時間以外の何かしらの値でバージョニング(というよりはタグ付与)がしたいです。
TemplatedConfigLoaderによる設定への値の注入
0.17.0から実装された機能です。
yamlにプレイスホルダーを設定でき、値をコードから注入できます。外部にプレイスホルダーへのマッピングを記述したyamlを用意することでも注入できます。
これを使うことでcatalog.yml
におけるfilepath
を直接変更し、バージョニングを実現します。
バージョンごとに保存するパスを変更することでバージョニングを実現しようという腹です。
また、これによって、全てのデータセットが一律のバージョン名であることを強制されていましたが、データセットに応じてバージョン名を変更することができます。
TemplatedConfigLoader
はhydraの簡易版ぐらいの機能です。
hydraとはyamlの構成管理のツールの一つです。コマンドラインからの値の注入やyamlの構造化などを実現できるようにするOSSです。
hydraはhydraで独特の記法を要求され、その書き方がkedroと相性がちょっと悪いのでkedroでテンプレート機能を用意してもらえるのは便利ですね。
Data Catalogの用意の流れ
DataCatalog
が用意される時の処理を、プロジェクトテンプレートからプロジェクトを生成した場合について記述します。これによってどこにTemplatedConfigLoader
を適用すべきかを理解します。
プロジェクトテンプレートよりcliなどを用意した場合、kedroの実行セッションはKedroSession
と呼ばれるクラスによって生成されます。
細かいところは正しく説明しきれないので無視しますが、この時<project_name>/src/
配下にあるhooks.py
にあるProjectHooks
のインスタンスを参照して、記述されている各hooksを実行します。
このうちDataCatalog
に関わるhooksはregister_config_loader
とregister_catalog
の二つです。
前者がConfigLoader
を用意し、後者がDataCatalog
を用意します。
また、register_catalog
においてはregister_config_loader
を利用して読み込まれたcatalog.yml
に基づいてDataCatalog
を生成します。
以上からregister_config_loader
におけるConfigLoader
をTemplatedConfigLoader
に差し替えれば、保存するパスを動的に変更できそうなことがわかります。
実装
TemplatedConfigLoader
をregister_config_loader
に実装すれば、catalog.yml
を動的に変更できそうなことはわかりました。
実際にはCLIなどでバージョン名を操作したいので、その入力を受け付けられるようにします。
なので
-
ProjectHooks
クラスにバージョンに対応する変数を設定する -
register_config_loader
においてTemplatedConfigLoader
によって設定を読み込むようにする。この時catalog.yml
に値を注入するように変更する -
cli.py
でコマンドラインからバージョン名を受け取り、セッション生成前にProjectHooks
にバージョン名を渡すようにする -
catalog.yml
をTemplatedConfigLoader
から値を受け取れるようにプレイスホルダーを用意する
ということをします。
ProjectHooks
クラスにバージョンに対応する変数を設定する
class ProjectHooks:
_mode: str = ''
@classmethod
def set_mode(cls, mode: str):
cls._mode = mode
Session
ではここで生成されたhooksのインスタンスを呼び出すという仕様になってます。シングルトンパターンを採用されてないので、呼び出されても同一のインスタンスであることが保証できません。なのでバージョン名に対してクラス変数を設定しています。(変数名がmode
なのは実際のところ、自分は実行目的に合わせて利用するデータを変更したいだけであり正しくバージョニングしたいわけではないからです。)
register_config_loader
の変更
ProjectHooks
に次のインスタンスメソッドを実装します。
@hook_impl
def register_config_loader(self, conf_paths: Iterable[str]) -> TemplatedConfigLoader:
return TemplatedConfigLoader(conf_paths, globals_dict=dict(mode=self._mode))
これによって、mode
というプレイスホルダーを含んでいればそこにself._mode
の値を代入するようになります。
cli.py
でコマンドラインからバージョン名を受け取り、セッション生成前にProjectHooks
にバージョン名を渡すようにする
@click.option(
"--run-mode", type=click.Choice(['train', 'inference'], case_sensitive=True), default="train"
)
def run(
...,
run_mode
):
...
from .hooks import project_hooks
project_hooks.set_mode(run_mode)
(importは書く上で見やすいところに置いただけなので好きなところにおいてください)
catalog.yml
をTemplatedConfigLoader
から値を受け取れるようにプレイスホルダーを用意する
outputs:
type: pandas.CSVDataSet
filepath: s3://competitions/titanic/${mode}/outputs.csv
credentials: minio
実行
上記の変更で、kedro run --run-mode=train
とした時、ディレクトリtrainとしてきられています。
versioned
で制御できなくなってしまいましたが、まぁこの実装の時点で使わない方針なのでいいと思います。
まとめ
catalog.yml
を直接テンプレート化することでバージョニングを実装する方法を説明しました。
モデルについてのみ変更したい場合はまた別途モデル用のプレイスホルダーを用意して何らかの値を与えればよいです。
他の手段として、自前でVersionedDataSetを実装して、yamlの値で変更できるようにすることも考えましたが、結局プレイスホルダーを用意しないと動的に変更できないことからこの形にしました。
今後変更によって利用できなくなるかもしれませんが、一旦はこれでやっていこうと思います。
使っていて結構クリティカルに足りない機能な気がしたので、今後機能追加もありそうですね。(個人の感想です)
issueやPRを覗いてみると同じようなトピックがWIPで進んでいるのでそのうち追加されそうです。
雑に確認したので動かなければ教えて下さい。