お手軽で欲しい機能が揃っている実験管理ツールGuild AIの紹介
機械学習の実験管理ツールにGuild AIというものがあります。特に大きな特徴はコード追加なしで実験管理ができるというものです。
試しに触ってみたところ、まさにコード追加なしで簡単に試せる、ちょっとした条件を変えた実行も簡単、結果の可視化はシンプルなCLIもリッチなGUIもどちらも用意されている、ローカルだけではなくS3にもデータを保存できる、しかもWebサーバを別に立てる必要がなく手元の環境で完結します。
ただ、Guild AIは一部の方にSNS上で言及されているものの日本語で書かれたドキュメントやブログは見つかりませんでした。良いツールが埋もれるのはもったいないと思いGuild AIの記事を書くことにしました。
この記事の前半では実行条件を変えながら実行して結果を可視化するまでの流れを紹介します。Guild AIのお手軽さをお伝えすることを意識しました。
後半はある程度実務での状況を想定し複数の人が作業していても欲しい結果のみを参照する方法を紹介しています。
他にも紹介しきれない機能はあるのですが、最後にある資料からドキュメントを読んでみてください。簡単にですがここに書いていない機能にどんなものがあるかをリストにしてあります。
目次
この記事に書いていること、書いていないこと
- 書いていること
- 書いていないこと
- パイプライン実行
- 実験のパッケージング
- 実験情報をS3に保存する
- リモート環境で実行
- 他の実験管理ツールとの機能比較
ここで紹介しきれない機能は ドキュメントや examples をご覧ください。
GuildAIの紹介
Guild AIを手軽さを体験する
まず、guildaiパッケージをインストールしましょう。これで guild
コマンドがインストールされます。
$ pip install guildai
次に実行する度に違う値を返すrun_random.py
を用意します。これを実験用スクリプトとみなしてguildの手軽さを体験していきます。
import random start = 0 end = 10 print("result: %f" % random.uniform(start, end))
さっそく用意したスクリプトをguild run
で実行します。
$ guild run run_random.py Refreshing flags... You are about to run /home/jovyan/work/run_random.py end: 10 start: 0 Continue? (Y/n) y result: 4.966394
元コードに手を加えていないのにguild
コマンドがスクリプト中の start
と end
変数を認識していることがわかります。これはguildaiがスクリプトのグローバル変数の定義を読み込みflagとして認識するためです。
このまま何度か乱数を出力するスクリプトを実行してみます。
$ guild run run_random.py -y result: 7.314514 $ guild run run_random.py -y result: 2.449769 $ guild run run_random.py -y result: 1.361825 $ guild run run_random.py -y result: 6.973784
合計5回実行しました。それでは実行した結果を見てみましょう。 resultが小さいものから上位3つを取り出してみます。
$ guild compare --table --min result --top 3 run operation started time status label end start step result 9d9b1c1d run_random.py 2020-09-10 12:08:02 0:00:00 completed 10 0 0 1.361824 e0f6d49c run_random.py 2020-09-10 12:08:01 0:00:00 completed 10 0 0 2.449769 99a5dbbd run_random.py 2020-09-10 11:58:22 0:00:00 completed 10 0 0 4.966393
4番目に実行した1.361824が一番上に表示されています。
guildaiはkey: value
のフォーマットで標準出力に出された数値を認識します。これをguildaiではScalarsといいます。このフォーマットは設定で変更することもできます。
先程のスクリプトでは"result"をScalarsのkeyにしていたので--min result
を引数に指定することで生成した乱数を降順に取り出せました。機械学習の実験の場合は評価関数の結果を出力することでモデルの最適化に応用できます。
ここまででguildaiがどれだけ簡単に実験管理できるか雰囲気を感じることができたと思います。とはいえ乱数を出力するスクリプトを叩いただけではつまらないので次はもう少し実験らしいことをしてみます。
乱数を生成する条件を変えながら実行する
先程は0から10の間の数値を生成していましたがもう少し大きな値を取り出したくなったとしましょう。
end
を1000にして実行してみます。
$ guild run run_random.py end=1000 You are about to run /home/jovyan/work/run_random.py end: 1000 start: 0 Continue? (Y/n) y result: 258.577733
先程と同じく何度か実行してみます。
$ guild run run_random.py end=1000 -y result: 584.182183 $ guild run run_random.py end=1000 -y result: 111.938457 $ guild run run_random.py end=1000 -y result: 430.517918
実行した結果を確認しましょう。先程はCLIを使いました。CLIを使った確認は手軽ですが今回はguildaiの独自GUIであるViewで結果を確認してみましょう。
guild view
コマンドを実行します。実行したらブラウザから http://localhost:10000 にアクセスします。
$ guild view --port 10000 --host 0.0.0.0 Running Guild View at http://localhost:10000
ブラウザにアクセスすると個別の実験内容の詳細を確認できるダッシュボードが表示されます。
guildaiはこのView以外にもTensorBoardを組み込んでいます。Viewはguildaiの独自概念であるrunの可視化のために使い、パラメータの可視化は高機能なTensorBoardを使うという役割分担になっています。
Viewの左上のボタンからTensorBoardを開いて end
で与えたパラメータによって result
がどのように変化したか確認できます。
次はオープンデータのサンフランシスコの住宅価格データを使って実際にモデルを作る実験管理をしていきます。
オープンデータを使った実行
サンフランシスコの住宅価格データをダウンロードして保存します。
from sklearn.datasets import fetch_california_housing import pandas as pd data = fetch_california_housing() data_df = pd.DataFrame(data.data, columns=data.feature_names) target_df = pd.DataFrame(data.target, columns=["target"]) full_df = pd.concat([data_df, target_df], axis=1) from sklearn.model_selection import train_test_split def split_and_save(dataframe=None, target_path=None): train, validate = train_test_split(dataframe, test_size=0.3) train.to_csv(target_path + "/train.csv") validate.to_csv(target_path + "/validate.csv") split_and_save(full_df, "./data")
次に、住宅価格をxgboostの回帰で学習するスクリプト train_validate.py
を用意します。
import pandas as pd import xgboost as xgb train_data_path = "/home/jovyan/work/data/train.csv" validation_data_path = "/home/jovyan/work/data/validate.csv" train_df = pd.read_csv(train_data_path) validation_df = pd.read_csv(validation_data_path) x_train = train_df.drop("target", axis=1) y_train = train_df["target"] max_depth = 1 min_child_weight = 0.1 learning_rate = 0.1 clf = xgb.XGBRegressor(max_depth=max_depth, min_child_weight=min_child_weight, learning_rate=learning_rate) xg_model = clf.fit(x_train, y_train) x_predict = validation_df.drop("target", axis=1) y_predict = validation_df["target"] predict = xg_model.predict(x_predict) from sklearn.metrics import mean_squared_error mse = mean_squared_error(predict, y_predict) print("mse: %f" % mse)
乱数生成のスクリプトで試したようにmax_depth
やlearning_weight
などのパラメータを外部から与えてguild run
を何度も実行しパラメータ最適化をするんだろうと想像がつくかと思います。
しかし、この環境には乱数生成のスクリプトを使った実行結果が残っています。このまま実行すると今回の実行と前の実行結果が混ざって表示されてしまい確認したい結果に集中できません。 また、毎回違うパラメータを指定して何度も実行することは大変です。
そこで、サンフランシスコの住宅の予測モデルの生成スクリプトの実行にpredict-house-price
タグを付与して乱数生成の実行と区別がつくようにします。
さらにguildaiのパラメータ最適化の機能を使いそれぞれのパラメータの探索範囲を指定し最大20回のトライアルでランダムサーチを実行します。
guildaiはランダムサーチの他にグリッドサーチ、ベイジアン最適化をサポートしているので目的に合わせた使い分けができます。
$ guild run train_validate.py learning_rate=[0.01:0.1] max_depth=[2:4] min_child_weight=[0.1:0.3] --max-trials 20 --label predict-house-price Refreshing flags... You are about to run /home/jovyan/work/train_validate.py with random search (max 20 trials) learning_rate: [0.01:0.1] max_depth: [2:4] min_child_weight: [0.1:0.3] train_data_path: /home/jovyan/work/data/train.csv validation_data_path: /home/jovyan/work/data/validate.csv Continue? (Y/n) y INFO: [guild] Initialized trial 669896de (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv) INFO: [guild] Running trial 669896de: train_validate.py (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv) INFO: [numexpr.utils] NumExpr defaulting to 2 threads. mse: 0.348852 INFO: [guild] Initialized trial 2ce684f7 (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv) INFO: [guild] Running trial 2ce684f7: train_validate.py (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv) INFO: [numexpr.utils] NumExpr defaulting to 2 threads. mse: 0.332837
パラメータ探索の実行が終わったらguild compare
で結果を確認しましょう。-l predict-house-price
を指定することで結果を絞りこんでいます。
$ guild compare --table --min mse --top 5 -l predict-house-price run operation started time status label learning_rate max_depth min_child_weight train_data_path validation_data_path step mse 1f2013fe train_validate.py 2020-09-11 12:30:39 0:00:02 completed predict-house-price 0.095549 4 0.206510 /home/jovyan/work/data/train.csv /home/jovyan/work/data/validate.csv 0 0.242914 1a13f5bb train_validate.py 2020-09-11 12:30:35 0:00:02 completed predict-house-price 0.069911 4 0.254429 /home/jovyan/work/data/train.csv /home/jovyan/work/data/validate.csv 0 0.256736 673398fc train_validate.py 2020-09-11 12:30:10 0:00:02 completed predict-house-price 0.098693 3 0.163653 /home/jovyan/work/data/train.csv /home/jovyan/work/data/validate.csv 0 0.264573 8e83cb7b train_validate.py 2020-09-11 12:30:19 0:00:02 completed predict-house-price 0.098743 3 0.133931 /home/jovyan/work/data/train.csv /home/jovyan/work/data/validate.csv 0 0.266335 274f5aa2 train_validate.py 2020-09-11 12:29:56 0:00:02 completed predict-house-price 0.087953 3 0.184524 /home/jovyan/work/data/train.csv /home/jovyan/work/data/validate.csv 0 0.273010
TensorBoardでも確認してみます。ここでもラベルを指定して表示するrunを絞り込んでいます。
$ guild view -p 10000 -h 0.0.0.0 -l predict-house-price Running Guild View at http://localhost:10000
まとめ
guildaiをインストールしてコード追加なしにパラメータを変えた実行と結果の可視化、ラベルを使った実験管理の方法を紹介しました。
Kaggle用に良さそうなツールを探していたのですがguildaiが自分にいちばんハマる感覚があったので当分使ってみようと思います。
ちなみにguildaiは実験情報をS3に保存する機能があるのでminioを用意してそこに実験情報を保存してみようとしたのですがS3へのデータ保存が aws-cli を直接叩く実装*1になっていて保存先をminioに向けるための--endpoint-url
を外から与える手段が見つからなかったため諦めました。*2
資料
*1:https://github.com/guildai/guildai/blob/5d32cdf6eca60ad4195c7432612e5a86dc48f8e8/guild/commands/s3_sync_impl.py#L85-97
*2:/etc/hostsで名前解決を捻じ曲げればできそうな気もしますが...