機械学習を使ったプロジェクトに携わると、データ解析をしていくにつれて大量のモデルが構築され、これらのモデルを管理するだけでも大変です。IT企業のようなソフトウェアエンジニアリングの部署がある企業では、各社ベストプラクティスがあるのだと思いますが、私の場合、メーカー勤務かつ部署が研究開発ということもあり、ソフトウェアエンジニアリングのノウハウを学ぶことはできず、手探りで探索しているような状況です。となるとOSSを使うことになるのですが、機械学習のライフサイクル管理もかねてmlflowを本格的に使っていきたいと思います。
また、facebook researchが作成しているHydraと呼ばれるconfig管理フレームワークを導入します。これはargparseで書いていた部分をyamlで書くことで簡略化できるようになっています。今まではスクリプト上で煩雑だったものが、yamlで管理できるため非常に見やすくなります。
これら両方を用いて、ElasticNetのハイパーパラメータをグリッドサーチしていきたいと思います。
Hydra
インストール
pip install hydra-core --upgrade
使い方
まずはconfig.yaml
に設定ファイルを記載していきます。
params: alpha: 0.5 l1_ratio: 0.5 file: path: 'sklearn_elasticnet_wine/winequality-red.csv'
次に、python側では以下のようにして読み込みます。
import hydra from omegaconf import DictConfig, OmegaConf @hydra.main(config_name='config') def main(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
コマンドライン引数を読み込みたい関数にhydraをデコレートしてあげます。実行すると以下のようになります。
$ my_app.py params: alpha: 0.5 l1_ratio: 0.5 file: path: 'sklearn_elasticnet_wine/winequality-red.csv'
オーバーライドをするのも簡単です。
$ my_app.py params.alpha=1.0 params.l1_ratio=1.0 params: alpha: 1.0 l1_ratio: 1.0 file: path: 'sklearn_elasticnet_wine/winequality-red.csv'
コマンドライン引数を変更した場合はoutputs
のディレクトリが自動で生成されログが出力されます。
.hydra ├── config.yaml ├── hydra.yaml └── overrides.yaml
Multirun
また、複数の引数を実行することもできます。
$ my_app.py params.alpha=0.1,0.5,1.0 params.l1_ratio=0.1,0.5,1.0 [2021-03-06 15:49:32,780][HYDRA] Launching 9 jobs locally [2021-03-06 15:49:32,781][HYDRA] #0 : params.alpha=0.1 params.l1_ratio=0.1 [2021-03-06 15:49:32,979][HYDRA] #1 : params.alpha=0.1 params.l1_ratio=0.5 [2021-03-06 15:49:33,156][HYDRA] #2 : params.alpha=0.1 params.l1_ratio=1.0 [2021-03-06 15:49:33,351][HYDRA] #3 : params.alpha=0.5 params.l1_ratio=0.1 [2021-03-06 15:49:33,535][HYDRA] #4 : params.alpha=0.5 params.l1_ratio=0.5 [2021-03-06 15:49:33,722][HYDRA] #5 : params.alpha=0.5 params.l1_ratio=1.0 [2021-03-06 15:49:33,919][HYDRA] #6 : params.alpha=1.0 params.l1_ratio=0.1 [2021-03-06 15:49:34,095][HYDRA] #7 : params.alpha=1.0 params.l1_ratio=0.5 [2021-03-06 15:49:34,285][HYDRA] #8 : params.alpha=1.0 params.l1_ratio=1.0
このようにすることでグリッドサーチによるハイパーパラメータ探索のコードを簡単に記述することができます。
Mlflow
mlflowを用いれば各実行条件ごとのメトリック、モデル、ハイパーパラメータを適切にログを取ることができ、可視化をすることができます。今回はグリッドサーチですが、optunaのようなベイズ最適化を用いた探索の場合では、非常に簡単にログを取ることができます。
インストール
pip install mlflow
簡単な使い方
import os from random import random, randint import mlflow if __name__ == '__main__': with mlflow.start_run(run_id=0): mlflow.log_param("param1", randint(0, 100)) for i in range(3): mlflow.log_metric("foo", random() + float(i)) # Log an artifact (output file) if not os.path.exists("outputs"): os.makedirs("outputs") with open("outputs/test.txt", "w") as f: f.write("hello world!") mlflow.log_artifacts("outputs")
各計算結果をlog_metric
やlog_param
等で保存していきます。run_idは自動で生成されますが自分でつけることもできます。
mlflow + hydra
ネットで調べてみると、グリッドサーチを試してみたという例は複数ありました。私もすごく単純な例で試していきます。
ベースはmlflow/examples/sklearn_elasticnet_wineのサンプルコードです。 https://github.com/mlflow/mlflow/tree/master/examples/sklearn_elasticnet_wine
Metric
import os import warnings import logging import pandas as pd import numpy as np from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score from sklearn.model_selection import train_test_split from sklearn.linear_model import ElasticNet import mlflow import hydra from omegaconf import DictConfig logging.basicConfig(level=logging.WARN) logger = logging.getLogger(__name__) EXPERIMENT_NAME = 'elasticnet_wine' def eval_metrics(actual, pred): rmse = np.sqrt(mean_squared_error(actual, pred)) mae = mean_absolute_error(actual, pred) r2 = r2_score(actual, pred) return {'rmse': rmse, 'mae': mae, 'r2': r2} def fetch_data(cfg: DictConfig): data = pd.read_csv(hydra.utils.to_absolute_path(cfg.file.path), sep=";") train, test = train_test_split(data) # The predicted column is "quality" which is a scalar from [3, 9] train_x = train.drop(["quality"], axis=1) test_x = test.drop(["quality"], axis=1) train_y = train[["quality"]] test_y = test[["quality"]] return train_x, test_x, train_y, test_y
まずは基本的なパッケージを読み込みます。
mlflowで使う実験名も最初に指定しています。データやモデルの条件が変わった場合は実験名を変えるのかなと思います。
メトリックを計算する関数と、データを読み込む関数を指定します。hydraの注意点ですが、実行時にパスが自動的に変更されてしまいますので、hydra.utils.to_absolute_path()
を使って実行時のパスを取得してあげる必要があります。
Main
@hydra.main(config_name='config') def main(cfg: DictConfig): warnings.filterwarnings('ignore') np.random.seed(40) hydra_path = os.getcwd() train_x, test_x, train_y, test_y = fetch_data(cfg) alpha = float(cfg.params.alpha) l1_ratio = float(cfg.params.l1_ratio) lr = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42) lr.fit(train_x, train_y) predicted_qualities = lr.predict(test_x) metrics = eval_metrics(test_y, predicted_qualities) # MLFLOW os.chdir(hydra.utils.get_original_cwd()) client = mlflow.tracking.MlflowClient() # データごとにExperimentを作成 for exp in client.list_experiments(): if EXPERIMENT_NAME == exp.name: experiment_id = exp.experiment_id break else: experiment_id = client.create_experiment(EXPERIMENT_NAME) run_id = client.create_run(experiment_id).info.run_id for k, v in cfg.params.items(): client.log_param(run_id, k, v) for k, v in metrics.items(): client.log_metric(run_id, k, v) hydra_files = ['.hydra/config.yaml', '.hydra/hydra.yaml', '.hydra/overrides.yaml'] for hydra_file in hydra_files: client.log_artifact(run_id, os.path.join(hydra_path, hydra_file)) with mlflow.start_run(run_id=run_id): mlflow.sklearn.log_model(lr, "model") client.set_terminated(run_id)
注意点は、先ほどと同様にhydraが自動でログを作成するのでパスが変わってしまうことです。
os.chdir(hydra.utils.get_original_cwd())
でmlflowが生成するmlrusのディレクトリに保存されるようにします。
また、実験名が一致するディレクトリから実験idを取得します。run_idは実験idから自動で作成されます。
そのあとは、各ハイパーパラメータの値や、メトリックスの値をclientのメソッド方に渡してあげるだけです。 ついでにhydraの出力ファイルもアーティファクトの方に保存できるようにしています。
実行結果
下記コマンドでmlflowのUIが描画されます。内部ではreact.jsを用いています。
mlflow ui
以前にoptunaを試した例も載っていますが、計算結果がすべて保存されていることが分かります。
Scatter Plot, Contour Plot, Parallel Cordinates Plotも見れるようになっています。非常に便利です。
最後に
mlflowとhydraを使ってハイパーパラメータ探索を行いモデル管理まで行いました。効率よく機械学習モデルを作成して、デプロイまで考えるとなるとこういった技術は早く使えこなせるようになっていきたいと思います。
保存先もfileではなくてデータベースに保存したり、分散学習した場合のハイパーパラメータ管理など、こちらも手を付けていきたいと思います。