iMind Developers Blog

iMind開発者ブログ

MLFlowを使ってみる1 - tracking

概要

機械学習周りの処理のトラッキングとかモデルの管理をしたかったので比較的手軽に扱えそうな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(田みたいな絵のボタンで選択)にすると下記画像のような結果が取得できる。

f:id:imind:20190515200113p:plain

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, 記事投稿