【MLOps】SQLiteを使ってMLFlowに入門してみた（初心者向け）

# 0. はじめに

色々Pythonで機械学習の検証をしているときに、1回目と2回目の検証でどんなパラメータを使用して、どんな精度が出たのか？を記録するのにゴッチャで疲れてしまった。よって今更ながらMLFlowに入門してみたので**初心者でも分かるように**記録としてメモしておく。

- 動作環境
  *  OS : Windows10 pro
  *  python： 3.9.6　(pyenv)
  *  mlflow： 2.1.1
  *  PyGWalker： 0.1.4.3
  *  jupyter notebook(vscode)

# 1. MLOps
MLOpsとは、DevOps + Machine Learning の造語である。
簡単にだが説明する

## 1-1. DevOps（デブオプス）とは？

名前の通りですが、「Dev」と「Ops」の合体造語である。
つまり開発（Development）と運用（Operations）を組み合わせた言葉であり、開発におけるバージョン管理やデプロイ等を自動化する考え方である。


## 1-2. MLOps（エムエルオプス）とは？

DevOpsのDevを機械学習を表すMLに置き換えた造語である。
以下図は[Hidden Technical Debt in Machine Learning Systems](https://proceedings.neurips.cc/paper/2015/file/86df7dcfd896fcaf2674f757a2463eba-Paper.pdf)より図を抜粋したもので、これが概念を表すらしい。

![MLops.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/51baba1f-5568-da0a-5abf-b14bd2865718.png)

細かいことは専門の解説記事に譲るとして、私含めMLOps初心者は機械学習(ML)モデルの運用効率化だと思えばいいと思う。`※AI系は「PoC」で躓くことがあり、試行錯誤を繰り返す必要が多い分野なのでこのような考えが生まれたんだと思う`

# 2. MLFlow

## 2-1. MLFlowとは？

機械学習のワークフローの支援ツールでDatabrics社より提供されているOSS。
実験の試行錯誤の過程で生成されるものをまとめて視覚化可能で全部で4つの機能が存在するが、`本記事ではMLFlow Trackingの部分を説明`する。

- MLFlow
  *  MLFlow Tracking :　MLの実験結果整理と視覚化
  *  MLFlow Projects :　学習環境のパッケージ化
  *  MLFlow Models :　デプロイ環境を再現可能な形でパッケージ化
  *  MLFlow Registry :　モデルのバージョンやリリースの管理


## 2-2. 検証用サンプルデータ準備

今までよくこの手の記事でのサンプルとしてボストン住宅価格を使ってきた。が、これ無くなったみたいですね。。よって同じsklearnからアヤメのデータセットを準備します。

https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_iris.html#sklearn.datasets.load_iris

```python:アヤメデータの準備
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import pandas as pd

iris_data = load_iris()
iris_data_x = pd.DataFrame(iris_data.data, columns=iris_data.feature_names)
iris_data_y = pd.DataFrame(iris_data.target, columns=["y"])

# 後で使用するのでここで分けておく
X_train, X_test, y_train, y_test = train_test_split(iris_data_x, iris_data_y)

# これは2-3.のPyGWalker用に準備するだけでmlflowには不要
iris_df = pd.concat([iris_data_x, iris_data_y], axis=1)
iris_df.head(2)
```

| | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | y
--- | --- | --- | --- | --- | ---
|0 |5.1 | 3.5 | 1.4 | 0.2 | 0
|1 |4.9 | 3.0 | 1.4 | 1.4 | 0

## 2-3. PyGWalkerでTableau風にデータ確認

せっかくなので、自分用のメモを兼ねてこのアヤメデータを最近話題になったPyGWalkerを使って確認してみる。
`ただし、ここはこの記事の本質とは違うので折りたたみにしました。`興味ある方は開いて確認ください。

<details><summary>PyGWalkerでTableau風にデータ確認してみる</summary><div>
<br>

**ここだけGoogle Colabを使用しました。**
vscodeのjupyterだと可視化まではできますが、フィルタ調整できなかったり色々不具合があったので..

```python:PyGWalkerでTableau風にデータ確認
import pygwalker as pyg

# 上で作成したアヤメのdfを指定するだけでOK。セルに表示が出てくる
gwalker = pyg.walk(iris_df)
```

### 2-3-1.Dataを編集
まずはDataを編集する。`これをやらないと思うように可視化できないので注意`
●dimention/measureの設定
measureは連続変数でdimentionは不連続な離散変数。今回は「分類」問題なので目的変数yは「カテゴリ」に設定しなおしておくこと。（数字なのでデフォルトは回帰問題と同じmeasureになっている）

●nominal/ordinal/qualitative/temporalの設定
nominalは離散変数の「名義尺度」のことで、分類の為にとりあえず数字を割り当てたものを指す。まさに今回のyがこれに当たる（数字は便宜上降られたものなので）
ordinalはnominalの「数字の大小の関係がある」バージョン。例えば成績の5段階評価の分類カテゴリ等が該当する（同じく離散変数）
qualitativeは↑２つとは違い、連続変数を指すもの。今回目的変数以外はこれに当たる。
temporalは時系列の概念。時間軸データに適応させる

![PyGWalker0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/6586ff4c-4f1b-6659-c761-54a7320fffe6.png)

### 2-3-2.Visualizationで可視化

`クドクド説明しても仕方ないので図を見てください。`
ぱっと見の分布とかを確認したい場合はかなり有用ですがカスタマイズがほぼできないと思った方がいいです（当然ですがTableauの劣化版）。
図のフィルタ部分見ても変なレンジになってますが、数字を直接書き込めないスライダ調整式なので、5~7にしたかったんですがこうなってしまいました(笑)

![PyGWalker1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/c92b68bf-5219-6900-6982-7ea5d5dba30c.png)

</div></details>


## 2-4. MLFlowで実験管理

MLFlowでは実験結果を格納する場所が必要。今回はPythonにデフォルトで付属している`SQLite3`をデータベースとして利用することにする。

https://mlflow.org/docs/latest/tracking.html
![sqlite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/20b3f5cb-56f1-6b2e-fec8-6716e702192d.png)


インストールは`pip install mlflow `で行う。


### 2-4-1. MLFlowのExperimentsを設定する

**検証する実験環境をExperimentsと呼ぶ**
今回は「ランダムフォレスト」を使ってアヤメを分類する検証環境を作成することにする。
なおフォルダ構成は以下のようになっている(`dbフォルダを直下に事前作成してる`)
![構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/aa43ad6a-3cf2-129f-c41e-6550591a1f7b.png)


```python:mlflowの検証環境(Experiments)を準備する
import mlflow
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import r2_score, mean_squared_error, confusion_matrix, accuracy_score
import matplotlib.pyplot as plt
import seaborn as sns

"""
各種設定を行う
Experiments名称は好きにつければいい
DB名称はmlruns.db ※今回はdbフォルダ直下に作成
loggingフォルダ名はmlrunsとして直下に勝手に作成される(ので指定しない)
実験を記録するバックエンドの場所(URI(Uniform Resource Locator))としてsqliteを指定
"""
# Experiments名称
EXPERIMENT_NAME = "ayame_RandomForest"
#トラッキングサーバのURI場所を指定してmlflowに設定
DB_PATH = 'db/mlruns.db'
tracking_uri = f'sqlite:///{DB_PATH}'
mlflow.set_tracking_uri(tracking_uri)

# 実験名を取得する ※初回だから存在しない
experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)

# 当該Experiment存在しないときは新たに作成してidを取得し、すでに存在する場合はidを取得する
if experiment is None:
    experiment_id = mlflow.create_experiment(name=EXPERIMENT_NAME)
else:
    experiment_id = experiment.experiment_id
```

これを実行するとExperimentsが作成されるので、ここで確認してみる。**Mlflowがインストールされている仮想環境内で以下コマンドを入力する(今回はdb/mlruns.dbなので指定)**
```:コマンドプロンプト
(.venv) mlflow ui --backend-store-uri sqlite:///db/mlruns.db
```

```:出力
INFO:waitress:Serving on http://127.0.0.1:5000
```
サーバーURLが表示されるので`http://127.0.0.1:5000`へアクセスすると以下のような画面がブラウザ表示される。
デフォルトで`Default`という環境ができてしまうが、今回設定した`ayame_RandomForest`の環境も確認できる。

![experiment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/74e23173-5169-cc84-df8a-ea11b7084eb7.png)

なお、experiment_idはこのExperiments管理番号的なやつで一応確認してみる。

```python:ExperimentsのIDとは？
EXPERIMENT_NAME = "Default"
experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)
experiment_id = experiment.experiment_id
print(f"{EXPERIMENT_NAME}の管理番号は{experiment_id}です")

EXPERIMENT_NAME = "ayame_RandomForest"
experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)
experiment_id = experiment.experiment_id
print(f"{EXPERIMENT_NAME}の管理番号は{experiment_id}です")
```
```:実行結果
Defaultの管理番号は0です
ayame_RandomForestの管理番号は1です
```

### 2-4-2. MLFlowのRunに検証記録を残す(1回目)

**Experimentの下で管理される実行環境をRunという**
ここに1回目の検証を記録してみる（run_nameにそれを分かるように命名しました）

```python:RandomForestの1回目検証をMLFlowのRunに記録
# この検証を記録するExperiment_idを取得する
EXPERIMENT_NAME = "ayame_RandomForest"
experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)
experiment_id = experiment.experiment_id

with mlflow.start_run(experiment_id=experiment_id, run_name="RandomForest_1回目") as run:
    # 1回目の実験パラメータ
    n_estimators = 100
    max_depth = 5
    max_features = 2
    random_state = 77

    # Autologの無効化(Autologは後ほど・・)
    mlflow.sklearn.autolog(disable=True)

    rf = RandomForestClassifier(n_estimators = n_estimators, max_depth = max_depth, max_features = max_features,random_state=random_state)
    rf_model = rf.fit(X_train, y_train)

    # パラメータをmlflowに記録する
    mlflow.log_param("num_trees", n_estimators)
    mlflow.log_param("maxdepth", max_depth)
    mlflow.log_param("features", max_features)
    mlflow.log_param("seed", random_state)

    # モデルをmlflowに記録する 
    mlflow.sklearn.log_model(rf_model, artifact_path=EXPERIMENT_NAME, input_example=X_train)

    # モデルで予測する
    pred_test = rf.predict(X_test)

    # 評価指標をmlflowに記録
    acc = accuracy_score(y_test, pred_test)
    mlflow.log_metric("acc", acc)

    # 混同行列をSeabornで作成
    fig = plt.figure()
    cm = confusion_matrix(y_test, pred_test)
    sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
    plt.xlabel("Pred", fontsize=13)
    plt.ylabel("True", fontsize=13)

    # log_figureで混同行列をmlflowに記録する
    mlflow.log_figure(fig, "test_confusion_matrix.png")
```

さてこの後ブラウザを更新してみると、RunとしてRandomForest_1回目が記録されていることがわかる。

![1回目.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/02c40d77-c302-e1e0-7b2f-4af8979f3e2f.png)

さらにRun(RandomForest_1回目)をクリックすると、先程記録したものが格納されていることがわかる。
なお、モデルと画像は実際にはローカルPCの`./mlruns/Experiment_id/Run_id/artifacts`に格納されている

![1回目結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/c7017725-8e5b-f81e-a041-a61befc0448b.png)


### 2-4-3. autologで全パラメータを勝手に記録する(2回目)

2回目以降の検証も同様であるが、今回は`mlflow.**.autolog`の機能を使ってみる。
基本的に学習時の記録がすべて残る設定だと考えればいいと思う。
なお`log_param`や`log_metric`等も書かなくていいが、下記例の様に追加で書いてもいい。

なお`mlflow.**.autolog(disable=True)`でAutologを無効化可能

```python:2回目の検証をautolog()で記録
with mlflow.start_run(experiment_id=experiment_id, run_name="RandomForest_2回目") as run:
    # 2回目の実験パラメータ
    n_estimators = 100
    max_depth = 5
    max_features = "auto"
    random_state = 7

    # Autologのを有効にする ※log_modelと同じsklearnを指定する
    mlflow.sklearn.autolog()

    rf = RandomForestClassifier(n_estimators = n_estimators, max_depth = max_depth, max_features = max_features,random_state=random_state)
    rf_model = rf.fit(X_train, y_train)

    # パラメータをmlflowに「追加で」記録する(こんな感じでメモとかも書ける)
    mlflow.log_param("Memo", "seedとmax_features変えてみた")

    # モデルで予測する
    pred_test = rf.predict(X_test)

    # 評価指標をmlflowに「追加で」記録
    acc = accuracy_score(y_test, pred_test)
    mlflow.log_metric("test_acc", acc)

    # 混同行列をSeabornで作成
    fig = plt.figure()
    cm = confusion_matrix(y_test, pred_test)
    sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
    plt.xlabel("Pred", fontsize=13)
    plt.ylabel("True", fontsize=13)

    # log_figureで混同行列をmlflowに「追加で」記録する
    mlflow.log_figure(fig, "test_confusion_matrix.png")
```


すると、2回目は以下のように記録されている数が増えていることがわかる。
Metrixは学習後の評価指標が色々出ており、さらに学習時の混同行列まで保存されてますね。
ただ正直数が多いのであまりお勧めできないかも・・？？

![2回目.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/e4071004-22ea-ff6e-fe3e-c4c14204a170.png)


### 2-4-5. 1回目の学習モデルを呼び出して予測する

モデルの呼び出しは簡単で、ブラウザの1回目の部分に呼び出し方が書いてあるのでコピペするだけでいい。

![予測.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/8c77e415-ef83-b501-b694-24f6c768eff3.png)


```python:1回目のモデルを呼び出して予測する
model_name = "ayame_RandomForest"
run_id = "＊＊＊＊＊＊＊＊"

logged_model = f'runs:/{run_id}/{model_name}'

# Load model as a PyFuncModel.
loaded_model = mlflow.pyfunc.load_model(logged_model)

# 予測する
pred_test = loaded_model.predict(X_test)
acc = accuracy_score(y_test, pred_test)
print(acc)
```

実行結果は当然1回目に`mlflow.log_metric("acc", acc)`として記録している正解率と同じことがわかる。

```:実行結果
0.9736842105263158
```

# 3 おわりに

割と詳細に書いてきたので、思ったより簡単に記録できることが理解していただければ幸いである。忘れっぽい私も今後はこれを活用することで正しく検証の管理をしていこうと考えている。
それでは今回はここまで。



# 参考

https://github.com/mlflow/mlflow/tree/master/examples

https://mlflow.org/docs/latest/index.html

https://www.databricks.com/notebooks/gallery/MLflowLoggingAPIPythonQuickstart.html