お手軽で欲しい機能が揃っている実験管理ツールGuild AIの紹介

機械学習の実験管理ツールにGuild AIというものがあります。特に大きな特徴はコード追加なしで実験管理ができるというものです。

試しに触ってみたところ、まさにコード追加なしで簡単に試せる、ちょっとした条件を変えた実行も簡単、結果の可視化はシンプルなCLIもリッチなGUIもどちらも用意されている、ローカルだけではなくS3にもデータを保存できる、しかもWebサーバを別に立てる必要がなく手元の環境で完結します。

ただ、Guild AIは一部の方にSNS上で言及されているものの日本語で書かれたドキュメントやブログは見つかりませんでした。良いツールが埋もれるのはもったいないと思いGuild AIの記事を書くことにしました。

この記事の前半では実行条件を変えながら実行して結果を可視化するまでの流れを紹介します。Guild AIのお手軽さをお伝えすることを意識しました。

後半はある程度実務での状況を想定し複数の人が作業していても欲しい結果のみを参照する方法を紹介しています。

他にも紹介しきれない機能はあるのですが、最後にある資料からドキュメントを読んでみてください。簡単にですがここに書いていない機能にどんなものがあるかをリストにしてあります。

目次

この記事に書いていること、書いていないこと

  • 書いていること
    • Guild AIの手軽さを体験する
    • 条件を変えながら実行する
    • Optimizerを使って複数の実験条件をバッチ実行する
    • CLIを使った実行結果の管理、検索
    • GUIを使った実行結果の管理、検索
    • 他の実験と区別した実行、検索
  • 書いていないこと
    • パイプライン実行
    • 実験のパッケージング
    • 実験情報を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コマンドがスクリプト中の startend 変数を認識していることがわかります。これは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

ブラウザにアクセスすると個別の実験内容の詳細を確認できるダッシュボードが表示されます。

f:id:kuromt:20200910212807p:plain

guildaiはこのView以外にもTensorBoardを組み込んでいます。Viewはguildaiの独自概念であるrunの可視化のために使い、パラメータの可視化は高機能なTensorBoardを使うという役割分担になっています。

Viewの左上のボタンからTensorBoardを開いて end で与えたパラメータによって result がどのように変化したか確認できます。

f:id:kuromt:20200910213558p:plain

次はオープンデータのサンフランシスコの住宅価格データを使って実際にモデルを作る実験管理をしていきます。

オープンデータを使った実行

サンフランシスコの住宅価格データをダウンロードして保存します。

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_depthlearning_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

f:id:kuromt:20200911213710p:plain

まとめ

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で名前解決を捻じ曲げればできそうな気もしますが...