サマリ
- MLDP(後述)を読んだ流れでMLOps系のライブラリが気になり、手始めにMLflowを動かすことにした
- どうせなので、データ取得からモデル訓練、評価までの一連を全部動かせる仕組みをDocker-composeにまとめることにした
- 特異性が有ることをやった訳でもないが、それなりに勉強になったので記録を残す。
背景
はじまりは、以下書籍が自分の中で2021年上期アツかった事からはじまる。
ちょうど自身も昨年頃からはML実運用系の業務に関わることが増えてきたので、本書で書かれている内容は「あっやっぱり運用に乗せるシステムを考えると、そういうやり方になるのね」という丁度いい驚きが有った。その辺から「MLOps」という言葉にアンテナを張りはじめ、ちょっとベンダ系の製品を触ったあと「OSSだとどうなってるんだ?」というのが気になってきたので、ちょっと自分で使ってみるか、となったのである。
考えること(デザイン)
やってみたいことの核は「MLflowを継続して動かす」である。とはいえ、どうせ動かすなら、ちょっとしたトイプロブレムにしてみようと考え、こんな構成にした。
- Airflowが日次でワークフローをキックして、最新のデータをスクレイピングで取得する。
- 次のワークフローで取得したデータをもとにモデル訓練、評価をキックする。
- MLflowに訓練、評価の記録を取る
要は「毎日データが増えて、その度にモデルの訓練をすることに意味がある」問題であれば適用できるような構成だ。すると必然的にMLflowに記録を取る訓練タスク、その前段階のデータ取得タスクをスケジュール実行する必要がある。ここが迷いどころと言うか振れ幅で、普通に考えればcronジョブの利用が真っ先に浮かぶのだが、自分としてはなるべくDockerにまとめて再現性のある環境にしたかった。そのあたりもPythonでコントロールしたいな、という所で、前から別の書籍で名前は知っていたAirflowを使うことにした。(正直Luigiやgokartも気になったのだが、Airflowで一旦動いたのでそれで良しとする)
余談だが、実際に私自身が手元でコードを動かす際、実際にやったのは「日次で大谷翔平くんの今シーズン成績を取得し、週間成績から翌日の試合でHRを打つかどうかを予測するランダムフォレストモデルを作成する」だった。後述するコードでは(スクレイピングで特定サイトを取得するという性質上)スクレイピング部分について再現できる状態での公開、記述はしない。(モデルそのものもかなり適当だし…まぁ今回そこは主眼ではないので割愛)
やったこと(それぞれの実装の流れとつまりどころめも)
実装コードは下記の通り。 github.com
ここからは各コンポーネントについて、順を追ってやったことや詰まりどころを記載していく。
スクレイピング処理(scrape)
- シンプルにlxml.htmlでCSSセレクタを使ってパースするだけの仕組みにした。あんまり場数踏んでいないので、過去に自分の作ったコードを改変してささっと。
- 最初はScrapyを使おうかとも思ったが、少し詰まったのと、そもそも大層な仕組みにするほどデータ取らないな…と気付く
- 取得データの表現に@dataclassを使った。classの定義が楽なのは実感できたが、色々とメソッドを足しこみすぎてブレてしまった…
- 最終的にデータをcsvに吐いて後続処理で使う。csvではなくsqliteでも良かった気はする。
with MLflowでモデル訓練〜推論(mlproject)
- モデル訓練〜推論部分は言及すること無いので割愛(ただただ雑な前処理したデータをRandomForestClassifierにブチ込んでいるだけだ…)
- mlflowだが、訓練コードの中で以下のように呼び出す使い方、構造になっている。
import mlflow mlflow.set_tracking_uri('http://localhost:5000/') # tracking結果を保存する先のサーバー、指定がない場合はローカルにファイルを吐く mlflow.set_experiment("my-experiment") # Experiment(実験)の名前を決める with mlflow.start_run() as run: # Runの開始、ここからRunIDが採られてExperimentの下に増えていく random_forest.fit(train_x, train_y) #こんな感じで訓練したあと mlflow.log_param(key='key', value=huga) # 記録したいパラメータをログする
- 訓練コードの中でmlflowのclientを呼び出すことで、ハイパーパラメーターや訓練後のMetric情報をログしていく。
- 概念として理解しておくとよいのは「Experiment」と「Run」あたりだろうか。1:nの関係で、Experimentに記録したい実験内容(「大谷翔平くん打撃成績予測」みたいな)が入るようにすると、その下に訓練が行われる度にRunが1,2,3...と増えていく。
本質的には「高級なlogger」なのかもしれないが、ある程度仕組みが用意されていて簡単に記録できるのは、使ってみると思った以上に頼もしい。
scikit-learnからmlflowを使う場合は、その名も
mlflow.sklearn
とかいうまんまのモジュールが用意されている。- https://www.mlflow.org/docs/latest/python_api/mlflow.sklearn.html
mlflow.sklearn.autolog()
を実行して、mlflow.sklearn.eval_and_log_metric()
を使うと、とりあえず余り意識せずメトリックの記録、モデルの保存までやってくれる。楽ちん。- 何をやっているのか把握するという意味では、普通のtutorialとも見比べて自分で
mlflow.log_param
も書いてみても良いとは思う
- 何をやっているのか把握するという意味では、普通のtutorialとも見比べて自分で
MLflow serverをDockerで建てる(mlflow-server)
- 基本的な使い方は前項の通りで、後はTutorial等々を進めれば簡単に使えると思う。
- 個人的に今回ハマったのはmlflow serverのDocker構築。
- mlflowそのものは「ローカルでも使えるし、サーバー建てて複数人でも使えるよ」という方式になっているのだが、どうせならサーバ化してWeb UIでさっくり実験結果を見れるようにしたかったので、Dockerでmlflow-serverを建てることにした。
- 公式サイトで言うと、以下のシナリオになる。
- mlflowのDockerイメージは、なぜか公式RepoのDockerfileのものが動かず、おそるおそる自作してみた所動いたのでそちらを使った。
FROM python:3.8-slim-buster RUN pip install --upgrade pip RUN pip install mlflow==1.19.0 \ && pip install matplotlib==3.4.2 \ && pip install sqlalchemy==1.4.22 \ && pip install scikit-learn==0.24.2 \ && pip install pandas==1.3.1 \ && pip install cloudpickle==1.6.0
- しかしメチャクチャハマってしまったのがartifactの置きどころで、
mlflow server
の--default-artifact-root
をどう指定してもDockerイメージの中に落ちてこない。- 何日か格闘した末、日本語blogでズバリの解説を見つけた
一方で、--default-artifact-root はサーバに接続してきたクライアントに「Artifact はここに保存してね」と伝えられる場所に過ぎない。 つまり、MLflow Server がプロキシしてくれるわけではないのでクライアントから接続性のある URI を指定する必要がある。
Python: MLflow Tracking を使ってみる - CUBE SUGAR CONTAINER
- つまるところ、mlflowにトラッキングを投げる側のclientコード(今回だと推論を行う方)から見えるパスじゃないと駄目…ということを理解し、
/mlruns-artifacts/
というパスが見えるようにDocker-compose側のマウントを調整して対応した。
Airflow環境をDockerで建てる(airflow-server)
- ここまで実装できれば、後は関連するPythonプログラムを順に実行していく仕組みを作れればいい。別項で述べたとおりAirflowほどリッチな仕組みはあまり必須ないのだが、とりあえず動いたのでAirflowを採用した。
- ここまで来ると全部Dockerでまとめたかったので、Dockerで環境を作る。公式サイトで案内されている方法はDocker-composeで複数コンテナを建てる方法で、今回は単一にまとめたかったので(逆説的だが)localのInstall手順を元にDockerfileを作成した。
- https://github.com/shinebalance/myproto-mlflow/blob/main/airflow-server/Dockerfile
FROM python:3.8-slim-buster RUN pip install --upgrade pip # Airflow実行環境 ENV AIRFLOW_HOME=/opt/airflow RUN pip install "apache-airflow==2.1.2" --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.1.2/constraints-3.8.txt"
- あとは手順通り、db initとuser createをすれば動く。
AIRFLOW_HOME
以下は外部マウントして永続化しておき、airflow scheduler
は-D
を付けて起動時に実行しておけば、後は普通に利用できる。
Airflowでワークフローをバッチ実行する
- Airflow自体は事前にDAGとよばれる有向非巡回グラフを定義して、そこにジョブを書いていく。こう表現すると敷居高く感じるが、後述のコード抜粋と出来上がったGraph viewを見るとなんとなくわかると思う。
- これも公式チュートリアルや紹介blogを見て動かした後、ささっと自作した。
- DAG自体にも色々使えそうなオプションがありそうだったが、今回はML関係の環境を
pipenv
で切り分けたかったので、BashOperator
からpipenv run
するだけの超超お手軽構成。
dag = DAG( 'mlproject_run_hooktack', default_args=default_args, description='データの取得、モデルの訓練、推論を行うワークフロー', schedule_interval=timedelta(days=1), ) t1 = BashOperator( task_id='getdata', bash_command='cd /opt && pipenv run python scrape/req2url.py', dag=dag, ) t2 = BashOperator( task_id='sleep', depends_on_past=False, bash_command='sleep 5', retries=3, dag=dag, ) t3 = BashOperator( task_id='train', bash_command='cd /opt && pipenv run python mlproject/train_and_predict.py', dag=dag, ) t1 >> t2 >> t3
Docker-composeでまとめる
- ここまで実装できれば、あとは全部まとめて動くようにするだけである。
- 最終的にはAirflow,MLflowが使うディレクトリ類を片っ端から親ディレクトリにマウントしまくる構成になった。こうしてオレオレDockerはできていくのだな…
version: '3' services: mlflow-server: build: ./mlflow-server container_name: mlflow-server volumes: - ./mlflow-server:/mlruns/ - ./mlruns-artifacts:/mlruns-artifacts/ ports: - "5000:5000" environment: TZ: Asia/Tokyo airflow-server: build: ./airflow-server container_name: airflow-server volumes: - ./airflow-server:/opt/airflow/ - ./scrape:/opt/scrape/ - ./mlproject:/opt/mlproject/ - ./logs:/opt/logs/ - ./data:/opt/data/ ports: - "5005:5005" tty: true environment: TZ: Asia/Tokyo
まとめ
もともとはMLflowを学ぼう、というモチベーションだったのだが、結果的に色々とオレオレ改造した構成になってしまった。とはいえ一つモノが出来ると改善の足がかりは色々出てくるので、今の自分の実力値の記録ということで一旦はこれで完成とする。 改善したいポイントはこのへん。個人的メモの要素が強いが一応。
- Airflow → 機能がリッチすぎる&いちいち癖があるきがする。Luigiかgokartあたりに変える
- 共通 → ちゃんとgunicorn使う
- スクレイプ処理 → csvではなくDB化したい
- 訓練 → そもそもモデルがクソなので改善
- 推論 → 推論結果をjsonに出力して静的サイトで読んだほうが早い気がしてる
以上。