🗃️

実験結果管理ツール「Resutil」

2024/07/06に公開

なぜ「Resutil」を開発したのか

Pythonを機械学習やデータ処理を目的にされている方の多くはデータの管理に頭を悩ませているのではないでしょうか。私もその一人でした。

機械学習やデータ処理では入出力データが膨大な量になります。機械学習であればトレーニングデータやモデル、ログなどにあたります。※本記事ではこのようなデータをまとめて「実験データ」と呼ぶことにします。

私はこれまで、data/とかresults/のようなフォルダをつくり、さらにサブロフォルダを用意してこのような実験データを保存していました。これらのデータは容量が大きいため、Gitリポジトリで管理しないのが普通です。しかし、ローカルストレージに貴重なデータが保存されているのは色々と不安なので、クラウドストレージへの定期的なバックアップをしていました。

プロジェクトも長くなってくると、他にも次のような問題がでてきます。

  1. そもそもその実験データが何を意図してでてきた結果なのかわからなくなる。
  2. ソースコードが変わっていくので、後で結果の再現ができなくなる。
  3. 得られた結果を加工してさらに別の解析を行っているようなときは、実験データ間の依存関係がわからなくなる
  4. 上記などの理由で他の人へプロジェクトを引き継ぐときに、実験データも一緒に引き継ぐのが面倒。

その他のツールを試しましたが、なかなかこれらの課題を解決はできませんでした[1]

そんなわけで、これらを解決するためのツールを開発することにしました。

名付けて「Resutil」です!

ResutilのGitHubリポジトリ

開発方針

まずは、自分自身のが実験データを生成したり利用しているときの行動を見つめ直して、ツールにどのような機能があればよいのか、どうすれば長く使えるものになるか、分析しました。

  • 既存のプロジェクトに超簡単に導入できる
  • 実験データのクラウドへのアップロードとダウンロードがスムーズにできる
  • 実験データの引き継ぎや実験の再現が確実にできる
  • 実験データの再現や引き継ぎが確実にできる

それぞれについてresutilがどのように解決したのかを詳細に説明をしていきます。

既存のプロジェクトに超簡単に導入できる

とにかく導入までの手数を減らさないとせっかく作ったツールを使うようになりません。自身のコードをresutilに対応させるまでの手数を最小限にして、さらにresutilを使うことで他の作業が楽になるようにしなければいけません。

具体的には次のようなステップで導入できるようにしました。

Step 1

PyPIからインストールする。

pip install resutil

Step 2

resutil init で初期設定をする。

$ resutil init
Input project name (resutil): MyProj       # プロジェクト名を入れる
Input directory name to store results (results): results   # 結果を入れておくフォルダを入力
Do you want to add .gitignore to results? (Y/n): Y         # このフォルダを.gitignoreに追加
Input storage_type (box): gcs                              # クラウドストレージのタイプを指定(ここではGoogle Cloud Storageを指定)
Input key file_path (key.json): key.json                   # サービスアカウントの秘密鍵が入ったjsonファイル(key.json)を指定
Do you want to add key.json to .gitignore? (Y/n): Y        # .gitignoreに追加(超重要)
Input folder id of base dir: 123456789012                  # boxのフォルダIDを入力
✅ Initialized.

初期設定が完了すると、resutil-conf.yamlという設定ファイルが作成されます。このファイルにはresutilの実行に関わる情報が保存されます(詳細は後述します)。あと最初のプロジェクトにresutilを導入するときのみクラウドストレージ側[2]の設定が必要です。

Step 3

ソースコードに以下のように変更します。

といっても、resutilをインポートしてメイン関数にデコレータ@resutil.main()を追加するだけです。

import resutil    # resutilをインポート

@resutil.main()   # main関数の前に追加
def main(params): # main関数の引数にparamsを追加
   
    # params.ex_dirに結果を保存するためのフォルダへパスが入る
    # ファイルを保存するコード等で利用してください。
    # たとえば、以下のように使います。

    with Path(params.ex_dir, "result.dat").open("w") as f:
        ......


if __name__ == "__main__":
    main()

実験データのクラウドへのアップロードとダウンロードがスムーズにできる

実験データのクラウドへのバックアップは通常は意識しないとついつい忘れがちですし、コードの試行錯誤をしているときはとくに面倒な作業を挟みたくありません。

Resutilを導入すると、実験データは自動的にクラウドへアップロードされるようになります。実行例をみてみましょう。

$ python your_code.py

✨ Runnning your code with Resutil

📦 Connected to Google Cloud Storage
  📁 Bucket name: resutil
  📁 Project dir: MyProj

📝 Input comment for this experiment (press [tab] key to completion): experiment_1

🚀 Running the main function...

[ここにオリジナルコードの実行結果が表示される]

🗂️ Uploading: aapiyl_20240706T064339_experiment_1
ℹ️ There are 11 other experiment directory(s) that have not been uploaded.
✅ Done

流れとしては以下のようになっています。

  1. 使用されるクラウドストレージの情報が表示される
  2. 実行時のコメントを入力する(あとで実験データのフォルダ名に使われる)
  3. オリジナルコードが実行される
  4. クラウドストレージにアップロードされる

実験データを格納するフォルダ名はresults/aapiyl_20240706T064339_experiment_1のようになりますが、ちょっとしたこだわりポイントです[3]

プログラムの実行が終了すると、実験結果を入れたフォルダはクラウドストレージに自動アップロードされます。クラウドストレージはGoogle Cloud Storage、Google Drive、Boxに対応しています。(もちろん他のストレージにも対応したいと考えています。)

クラウドストレージ上には実験データはzipで圧縮されて格納されています。実験データは細々したファイルが多くなる傾向があるため、通信効率やストレージ利用効率をよくするためです。

逆に実行データのダウンロードも超スムーズです。他の人が実験データを取得したい場合等に使います。resutil pull [実験データのフォルダ名]と打ち込むだけです。resutil pull -Aコマンドでローカルに尊意しないすべての実験データをダウンロードすることができます。

resutil listコマンドを使うと、実験データの一覧が表示されます。
その実験データがローカルとクラウド上に保存されているかどうかが一目瞭然です。

% resutil list
📦 Connected to Google Cloud Storage
  📁 Bucket name: resutil
  📁 Project dir: MyProj

Remote | Local | Experiment name
   ✅  |       | aanohy_20240614T091818_experiment_1
   ✅  |       | aanrfk_20240615T180059_experiment_2
       |  ✅   | aapfty_20240704T185827_experiment_3
   ✅  |  ✅   | aapfty_20240704T185841_experiment_4
       |  ✅   | aapftz_20240704T185950_experiment_1-2
   ✅  |  ✅   | aapftz_20240704T185953_experiment_1-3
   ✅  |  ✅   | aapfua_20240704T190042_experiment_2-2
       |  ✅   | aapfub_20240704T190144_experiment_2-3
       |  ✅   | aapfub_20240704T190155_experiment_5

なおクラウドストレージの中は次のようなフォルダ構成になります。

BaseDir    # resutilのデータが入っているフォルダ(たとえばチームごとにつくる)
├──MyProj  # プロジェクトごとのフォルダ
│   ├── aakuqj_20240511T174522_ex1.zip  # 実験単位ごとのフォルダ(1)
│   │   ├── resutil-exp.yaml            # 実験に関する情報
│   │   ├── data.txt                    # 実験データ(例)
│   │   ...
│   │
│   ├── aamxrp_20240606T135747_ex2.zip  # 実験単位ごとのフォルダ(2)
│   │   ├── resutil-exp.yaml
│   │   ├── data.txt
│   │   └── uncommited_files            # gitにコミットされていないファイル
│   │       └── main.py
│   ...
│   
├──OtherProj
│
...

実験データの再現や引き継ぎが確実にできる

「あのときの結果がとても良かったけどどのコードをどうやって実行したんだっけな???」みたいなことはよく起こります。他の人への引き継ぎのときだったり、自分で結果を再現したいしたときに、ささっと再現したいものです。

実験データを再現するためには以下の情報が必要になります。

  • プログラムを実行したときのコマンド引数
  • Gitのコミットハッシュ
  • 未コミットファイルの中身
  • プログラム実行に必要なデータ

Resutilでは上記の情報をresutil-exp.yamlに保存します。ファイルの中身は次のようになっています。

args: your_code.py --input results/aanohv_20240614T091534_ex1  # 実行時のコマンド引数
dependency:                                                 # 依存する実験データのリスト
- aanohv_20240614T091534_ex1
git:
  commit_hash: a82ea4f8d3ed818d3c9cefbb24ba22b2eb329a0b   # Gitのコミットハッシュ
  uncommited_files:                                       # 未コミットファイルの中身
  - README.md                                             # これらのファイルはuncommited_files
  - src/resutil/config_file.py                            # にも保存されます
result_dir: results/aaoocn_20240626T115316_ex2

このファイルにはコード実行時のコマンド引数のほか、依存する実験データのリスト(次のセクションで説明)、Gitコミットのハッシュ、未コミットのファイル名が保存されます。コードはgit checkout コミットハッシュで取得できますし、コミットされれていなかったファイルそのものもuncommited_filesというサブフォルダが作成され、実験データと共に保存されています。これで実験を再現する情報はすべて揃っているはずです[4]

実験データの引き継ぎも簡単です。resutil-conf.yamlにResutilの設定が入っていますので、これをソースコードといっしょにGitで管理しておくだけです。

引き継ぎの際には次の3ステップで実験を再現する準備が整います。

  • git clone リポジトリURLでリポジトリをクローン
  • pip -r requirement.txtなどで必要なライブラリをインストール
  • クラウドストレージの秘密鍵をコピー

そして必要な実験データがあればresutil pullで取得します。(ここは実行時に自動で取得するようにしてもよいですね。次期バージョンにご期待ください。)

ちなみに、resutil-conf.yamlの中身は以下のようになっています。実験データ全体が入っているフォルダ名とクラウドストレージにアクセスするための情報が含まれています。ただし、サービスアカウントの秘密鍵key.jsonだけは機密情報ですので、うっかりGitHub等にアップロードしてしまわないように別管理としています。念の為resutil initで初期設定する際に.gitignoreに追記するようにしています。

project_name: MyProj
results_dir: results
storage_config:
  backet_name: 'resutil'
  key_file_path: key.json
storage_type: gcs

実験結果の依存関係が保持される

機械学習やデータ分析ではデータ間の依存関係が発生します。たとえば機械学習では、画像の生データを前処理のため加工して、トレーニングに用い、トレーニング済みのモデルから推論を行う、のようなパイプライン的なデータ処理をします。

AというデータからBを作り、さらにBからCというデータが得られるというケースを考えると、C→B→Aのような依存関係になります。

このような依存関係もresutil-exp.yamlに保存されます。

args: your_code.py --input results/aanohv_20240614T091534_ex1  # 実行時のコマンド引数
dependency:                                                 # 依存する実験データのリスト
- aanohv_20240614T091534_ex1
git:
  commit_hash: a82ea4f8d3ed818d3c9cefbb24ba22b2eb329a0b   # Gitのコミットハッシュ
  uncommited_files:                                       # 未コミットファイルの中身
  - README.md                                             # これらのファイルはuncommited_files
  - src/resutil/config_file.py                            # にも保存されます
result_dir: results/aaoocn_20240626T115316_ex2

dependencyというキーを見るとaanohv_20240614T091534_ex1という値が入っています。これはこの実験がaanohv_20240614T091534_ex1という実験データに依存していることを示しています。

したがってこの実験を再現するときには、依存する実験データresutil pull aanohv_20240614T091534_ex1で手動でダウンロードしてもよいですし、再現したい実験データaanohv_20240614T091534_ex1そのものをダウンロードするときに自動的にresutil pull aanohv_20240614T091534_ex1がダウンロードされます。

仕組みとしてはシンプルで、プログラム実行時に引数に実験データのフォルダっぽいものを抽出して、dependencyに保存しているだけです。この方法ではプログラム中にハードコーディングされたパスは認識できません。(組み込みのopen関数の引数を横取りすればある程度はできるかもしれませんが、これも次期バージョンにご期待ください)

また、入力データ(機械学習でいうデータセットなど)についてはinput/フォルダ的なものを作りがちですが、これもResutilで一元管理すると楽です。

resutil add コメントで新しい実験データ用フォルダが作成されます。ここに入力データを入れ、resutil push フォルダ名とすることでこの入力データに依存する実験に依存関係を保存することができます。

改良すべき点

まだ、Resutilは発展途上です。今後次のような改善をしていく予定です。

  • データのアップロード・ダウンロード時にプログレスバーがほしい(いつ終わるのかわからない)
  • 依存関係の取扱をもっとスマートにしたい
  • 実験データのハッシュを作成し、データの正当性のチェックしたい
  • 全体的に使いやすいUIにしたい
  • テストコードの作成(今は面倒なのでやっていない)
  • OSSとしての作法を学んで取り入れる

さいごに

実は今回のResutilの開発で、初めてPyPIに完全なオープンなライブラリとして登録してみました。思ったより簡単に公開できるんだなーという感想でした。

あと、他の人に使ってもらうという観点ができると、まだまだソフトウェアとしては不完全な点がたくさんあるということにも気付かされましたし、自分がほしいツールをOSSとして公開すると、もしかすると他の人がコメントをしてくれたり、改良をしてくれたりするかもしれないので、より良いツールを手に入れられるというメリットもあると思いました。

以下にResutilのPyPIとGitHubへのリンクを掲載しましたので、ぜひ使っていただき、コメントやプルリクエストをいただけるとありがたいです。


脚注
  1. 機械学習であればMLFlowやHydraのようなツールがあります。しかし、MLFlowは機械学習に特化してしまっていますし、Hydraはパラメータ管理ツールとしては優秀ですが、結果の整理までは手が届いていない印象でした。MLFlowについてはSQLサーバーを立てるまでやってはみたものの、自身のコードをMLFlowに対応させるまでの敷居がちょっと高く結局使わなくなってしまいました。Jupyterのようなノートブック環境を使うことも試しましたが、ソースコードのバージョン管理はほぼできないに等しいことと、実行順序がバラバラになりがちなので、少なくとも自分にはちょっとコードを試す用途以外では、使いこなせませんでした。 ↩︎

  2. 具体的にはストレージへのアクセス権があるサービスアカウントの設定をします。鍵をチームで共有する場合や個人で利用する場合には1回この作業をするだけで済みます。詳細はResutilのGitHubリポジトリのREADMEを参照してください。 ↩︎

  3. 20240706T064339は日時、experiment_1は実行時に入力するコメントです。aapiylは日時から決定する英字6文字で、シェル等でtabによる補完を容易にするものです。フォルダ名が日付で始まっていると、cdをするときに、年月日を入れなければいけませんが、一日に何度もプログラムを実行していると同じ年月日で始まるフォルダだらけになってしまい、補完が効かせるために、年月日を入力しないといけなくなるためです。しかもこの文字列は実行日時と同じ順番でソートされます。 ↩︎

  4. Pythonのバージョンや使用しているライブラリについても、Resutilで情報だけ保存しておいても良いかなとも思っています。最近はryeのような優秀なツールがあるので不要かもしれませんが・・・。ryeについてはまた別の記事で紹介したいと考えています。 ↩︎

Discussion