概要
機械学習周りの処理のトラッキングとかモデルの管理をしたかったので比較的手軽に扱えそうなMLFlowを試してみる。
本稿では基本機能の1つであるtrackingを用いて、scikit-learnでパラメータを変えつつ学習を行い、モデルや結果を保存する。
バージョン情報
- mlflow==1.0.0
導入
$ pip install mlflow sqlalchemy
DBを使って動かす際にsqlalchemyが必要になるので一緒に入れています。
MLFlowとは
mlflowは下記の3つの機能を持つ。
- MLflow Tracking - パラメータ等によってどうスコアが変化したかトラッキングする
- MLflow Projects - condaとかdockerとかgitと連携してプロジェクトをパッケージ化する
- MLflow Models - モデルの管理とかデプロイを行う
パラメータ変えながら学習してみてスコアがどう変わったか比較したり、実行環境を移植したり、生成されたモデルの中で良い結果のものをデプロイしたりといった日常的な作業をサポートしてくれる。
作業ディレクトリの作成
mlflowをtrackingするURLを指定せずに実行するとカレントディレクトリにmlrunsというディレクトリが生成され、その中に各種情報が記録される。
本項のサンプルコードはhome直下のworkというディレクトリで実行するものとする。
$ mkdir ~/work $ cd ~/work
scikit-learnでtracking
scikit-learnのlogistic regressionでCやsolverの値を変えつつ、precision, recall, f-scoreをトラッキングしてみる。データはwineのデータセットを利用する。
from itertools import product import numpy as np from sklearn import datasets from sklearn import model_selection from sklearn.linear_model import LogisticRegression import mlflow import mlflow.sklearn # datasetからwineを取得 X, y = datasets.load_wine(return_X_y=True) # wine_classifyという名前でmlflowのexperimentを生成 mlflow.set_experiment('wine_classify') tracking = mlflow.tracking.MlflowClient() experiment = tracking.get_experiment_by_name('wine_classify') # logistic regressionで利用するパラメータを用意 solvers = ['newton-cg', 'lbfgs', 'liblinear'] Cs = [1.0, 10.0, 100.0] # crossvalidateで算出するスコア scoring = ['precision_macro', 'recall_macro', 'f1_macro'] # solverとCの値を変えつつループ for solver, c in product(solvers, Cs): # experimentのidを指定してstart_run with mlflow.start_run(experiment_id=experiment.experiment_id, nested=True): # solverとCの値を設定してLogisticRegressionのestimatorを作る estimator = LogisticRegression( solver=solver, multi_class='auto', max_iter=30000, C=c) # cross_validateを使って各種スコアを算出 scores = model_selection.cross_validate( estimator, X, y, n_jobs=-1, cv=3, scoring=scoring, return_train_score=True) # cvごとのスコアをmeanしてmetricsに設定 mean_scores = dict([(k, v.mean()) for k, v in scores.items()]) mlflow.log_metrics(mean_scores) # solverとCのパラメータを設定 mlflow.log_param('solver', solver) mlflow.log_param('C', c) # 全件でモデル生成して保存 model = estimator.fit(X, y) mlflow.sklearn.log_model(model, 'wine_models') print(solver, c, '{:.5f}, {:.5f}, {:5f}'.format( mean_scores['test_precision_macro'], mean_scores['test_recall_macro'], mean_scores['test_f1_macro']))
scikit-learnのmodel_selection.cross_validateの結果は下記のようにn-foldしたそれぞれのスコアを返してくれます。
{'fit_time': array([0.253, 0.289, 0.281]), 'score_time': array([0.0015 , 0.0015, 0.0013]), 'test_precision_micro': array([0.716, 0.9, 0.982]), 'train_precision_micro': array([0.957, 0.94, 0.925]), 'test_recall_micro': array([0.716, 0.9, 0.98]), 'train_recall_micro': array([0.957, 0.940, 0.925]), 'test_f1_micro': array([0.716, 0.9, 0.982]), 'train_f1_micro': array([0.957, 0.940, 0.925])}
それぞれmeanしてスコアにしてしまう。
mean_scores = [(k, v.mean()) for k, v in scores.items()]
[('fit_time', 0.2749519348144531), ('score_time', 0.0014926592508951824), ('test_precision_micro', 0.8664750957854407), ('train_precision_micro', 0.9411016949152543), ('test_recall_micro', 0.8664750957854407), ('train_recall_micro', 0.9411016949152543), ('test_f1_micro', 0.8664750957854407), ('train_f1_micro', 0.9411016949152543)]
上記スクリプトを実行したら動作させたのと同一のパスでweb uiを立ち上げます。
$ mlflow ui
5000番ポートで接続。ポートを変えたい場合は -p ポート番号。
http://localhost:5000/
表示をgrid view(田みたいな絵のボタンで選択)にすると下記画像のような結果が取得できる。
mlrunsのディレクトリ構成
生成されたmlrunsディレクトリの中身を確認してみる。1の配下には複数のディレクトリが生成されるが、下記は1つだけ表示して他は省略している。
$ cd mlruns $ tree . ├── 0 │ └── meta.yaml └── 1 ├── 047ae2625152478aba207933fa700e9e │ ├── artifacts │ │ └── wine_models │ │ ├── MLmodel │ │ ├── conda.yaml │ │ └── model.pkl │ ├── meta.yaml │ ├── metrics │ │ ├── fit_time │ │ ├── score_time │ │ ├── test_f1_macro │ │ ├── test_precision_macro │ │ ├── test_recall_macro │ │ ├── train_f1_macro │ │ ├── train_precision_macro │ │ └── train_recall_macro │ ├── params │ │ ├── C │ │ └── solver │ └── tags │ ├── mlflow.source.name │ └── mlflow.source.type └── meta.yaml
初回実行であればmlruns配下に0と1の2つのディレクトリが生成されているはず。0は名前を設定されていないデフォルトのexperiment。1は今回生成したexperiment。
今回は実行する際に mlflow.set_experiment('wine_classify') のようにexperimentを設定している。このexperimentのidは下記のようなコードで取れる。
import mlflow tracking = mlflow.tracking.MlflowClient() exp = tracking.get_experiment_by_name('wine_classify') exp.experiment_id #=> 1
experimentディレクトリの配下には、uuidが名前になっているディレクトリが複数生成されている。これが1つずつの実行(run)の記録で、その配下にartifacts、metrics、paramsがファイルに記録されている。
├── artifacts ├── metrics ├── params └── tags (runの直下のフォルダのみ抜粋)
artifactはモデルファイルや画像等、どんな形式のファイルでも保存可能なフォルダ。
今回はコード内で mlflow.sklearn.log_model を使ってモデルを保存しており、artifact配下にはpickle形式のモデルが保存されている。
├── artifacts │ └── wine_models │ ├── MLmodel │ ├── conda.yaml │ └── model.pkl (artifactの配下)
metricsやparams配下にはそれぞれ設定した値が1ファイルずつに保存されている。ファイル管理をやめてsqliteやmysql等のRDBに保存することも可能。
SQLiteで動かす
mlflow serverを立ち上げてtrackingした情報がRDBに入るようにしてみる。
まずはwork配下で下記コマンドを実行し、ローカルでmlflowサーバを起動。--backend-store-uriはsqlalchemyのcreate_engineする時のURLを渡す。
$ mlflow server \ --backend-store-uri sqlite:///mlruns.sqlite3 \ --default-artifact-root artifact
ここでは2つの引数とも相対パスを指定しているので、コマンドを実行したディレクトリ配下にsqliteのファイルもartifactディレクトリも生成される。
実際の運用では絶対パスやs3等のリモート上のパスを指定することが多いと思われる。
立ち上げたserverに対してtrackingを行う実行する際は、set_tracking_urlを実行する。set_experiment等のtrackingに対する各種処理を実行するより前に設定する必要がある。
# tracking uriを設定 mlflow.set_tracking_uri('http://localhost:5000/')
set_tracking_urlした上でtracking処理を実行すると、mlflow serverを実行した際のカレントディレクトリにmlruns.sqlite3(引数の--backend-store-uriで指定した名前) というファイルが生成されている。
中身を覗いてみる。
$ sqlite3 mlruns.sqlite3 sqlite> .table alembic_version metrics runs experiments params tags sqlite> select * from experiments; 0|Default|artifact/0|active 1|wine_classify|artifact/1|active
先程はファイルで記録されていた情報がDBに入っていることがわかる。
artifactはDBには入らず、--default-artifact-root の指定パス配下にファイルとして置かれる。ファイルの置き場はS3、BLOB、SFTP、NFS、HDFSなどを指定することもできる。
runの取得
実行したrunの結果をPythonのコードで取得してみる。
まずはrunが所属するexperimentを取得。
tracking = mlflow.tracking.MlflowClient()
experiment = tracking.get_experiment_by_name('wine_classify')
exp_id = experiment.experiment_id
当該experimentのrun_infoを下記のように取得する。
tracking.list_run_infos(exp_id)
run_infoには下記のような情報が入っている。
key | description |
---|---|
artifact_uri | artifactの保存パス |
experiment_id | |
lifecycle_stage | activeやdeleted等のステータス情報 |
run_uuid | |
status | RUNNING, SCHEDULED, FINISHED, FAILED |
start_time | timestamp |
end_time | timestamp |
user_id |
get_runすると指定Runのmetricsやparamsが取れる。
run_info = tracking.list_run_infos(exp_id)[0] run = tracking.get_run(run_info.run_uuid) print(run.data.metrics) print(run.data.params)
実行結果(整形あり)
{ 'fit_time': 0.0042807261149088545, 'score_time': 0.002246220906575521, 'test_precision_macro': 0.9277878457780419, 'train_precision_macro': 1.0, 'test_recall_macro': 0.929488727858293, 'train_recall_macro': 1.0, 'test_f1_macro': 0.9255202327372435, 'train_f1_macro': 1.0 } {'solver': 'liblinear', 'C': '100.0'}
試しにparamsやmetricsを含めてrunの情報をpandasに放り込んで、lifecycle_stageがactiveで、statusがFINISHEDなRunの中でtest_f1_macroが最大のデータを抽出してみる。
import pandas as pd def get_run_info_and_data(run_info): run = tracking.get_run(run_info.run_uuid) # runの情報を取得 run_info = dict(run_info) # metricsの情報を追加 run_info.update(run.data.metrics) # paramsの情報を追加 run_info.update(run.data.params) print(run_info) return run_info run = [get_run_info_and_data(run_info) for run_info in tracking.list_run_infos(exp_id)] df = pd.DataFrame(run) df = df[(df.lifecycle_stage == 'active') & (df.status == 'FINISHED')].copy() df.sort_values('test_f1_macro').head(5)
こんな形でtrackingした各種情報は抽出できる。
artifactの取得
run uuidを指定してlist_artifactsすると当該runで保存したartifactの一覧が取れる。
run_uuid = '69a8d87a8c9b4a208f21850125bf4472' tracking.list_artifacts(run_uuid) #=> <FileInfo: file_size=None, is_dir=True, path='wine_models'>]
download_artifactを実行するとtemporaryファイルに指定のアーティファクトがダウンロードされる。戻り値は保存されたパス。
tmp_path = tracking.download_artifacts(run_uuid, 'wine_models') print(tmp_path) #=> /tmp/tmp8u8irw51/wine_models
scikit-learnのモデルであればsklearn.load_modelで直接取得することもできる。
mlflow.sklearn.load_model('runs:/' + run_uuid + '/wine_models')
改定履歴
Author: Masato Watanabe, Date: 2019-06-14, 記事投稿