0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルUbuntu Serverで実験管理環境を作る:MLflow / DuckDB / Hydra / Optuna / Prefect / DVCの使い分けについて

0
Posted at

ローカルUbuntu Serverで実験管理環境を作る:MLflow / DuckDB / Hydra / Optuna / Prefect / DVCの使い分けについて

はじめに

パラメータを少しずつ変えながら、同じような計算を何度も回すことがあります。
また、同時並行で複数業務をこなしていると、訳がわからなくなりますよね。

機械学習でも、数値計算でも、シミュレーションでも、最初のうちはCSVやフォルダ名で何とか管理できます。ところが、ケース数が増えてくると、だんだん次のような状態になります。

  • どのパラメータで実行した結果なのか分からない
  • 一番よかったケースを後から探すのが面倒
  • 結果CSVや図が増えすぎて整理できない
  • 入力データを少し修正した後、前の結果と比較しづらい
  • 再実行したいが、当時の条件が残っていない

このあたりをきちんと管理するために、ローカルのUbuntu Server上で、軽めの実験管理環境を作ることを考えます。

ここでは、次のツールを対象にします。

実験管理環境ツール

  • MLflow
  • DuckDB
  • Hydra
  • Optuna
  • Prefect
  • DVC

全部を最初から入れて使いこなす必要はありません。むしろ、最初は小さく始めて、必要になった段階で追加していく方がよいです。

全体のイメージ図です。
ChatGPT Image 2026年7月7日 09_36_43.png


全体の考え方

大まかには、次のように役割を分けます。

やりたいこと 使うツール 役割
パラメータを変えて何度も計算する MLflow 実験条件と結果の記録
計算結果を一覧比較したい MLflow runごとの比較、指標の確認
大量の時系列・表形式データを保存したい DuckDB 結果データベース
パラメータの全組み合わせを自動実行したい Hydra 設定管理、一括実行
最適なパラメータを自動探索したい Optuna パラメータ最適化
複数ステップの処理フローを管理したい Prefect ワークフロー管理
入力データや成果物のバージョン管理もしたい DVC データ・成果物の版管理

イメージとしては、MLflowを中心に置きます。

ChatGPT Image 2026年7月7日 09_35_04.png

Optuna、Prefect、DVCは、必要になってから追加する位置づけです。


まずはMLflow + DuckDB + YAMLで十分

最初に作るなら、次の3つでよいと思います。

MLflow : 実験管理
DuckDB : 大量の結果保存
YAML   : 設定ファイル管理

MLflowには、各実行ケースの条件と代表的な結果を保存します。

たとえば、次のようなものです。

parameters:
  case_name
  model_type
  dt
  learning_rate
  max_iter
  seed

metrics:
  rmse
  mae
  score
  runtime_sec
  status

artifacts:
  config.yaml
  result_summary.csv
  graph.png
  log.txt

一方で、詳細な時系列データや大きな計算結果はDuckDBに入れます。

MLflowに何でも入れると重くなります。MLflowは「実験台帳」、DuckDBは「結果データベース」と分けて考えると、後で扱いやすいです。


Ubuntu Server側の準備

ここではUbuntu Server 22.04 / 24.04あたりを想定します。
この準備に関しては、それぞれの環境があると思いますので、皆さんの環境に合わせてください。

まず、Pythonと基本ツールを入れます。

sudo apt update
sudo apt install -y python3 python3-venv python3-pip git build-essential

作業用ディレクトリを作ります。

mkdir -p ~/exp_management_local
cd ~/exp_management_local

Python仮想環境を作ります。

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

必要なライブラリを入れます。

pip install mlflow duckdb hydra-core optuna prefect dvc pandas pyarrow matplotlib pyyaml

バージョンを確認します。

python --version
mlflow --version
dvc --version
prefect version

フォルダ構成

最初は、これくらいの構成で十分です。

exp_management_local/
├─ configs/
│   └─ base.yaml
│
├─ data/
│   ├─ raw/
│   └─ processed/
│
├─ src/
│   ├─ run_experiment.py
│   └─ utils.py
│
├─ outputs/
│   ├─ figures/
│   ├─ tables/
│   └─ duckdb/
│       └─ results.duckdb
│
├─ mlartifacts/
├─ mlflow.db
├─ requirements.txt
└─ README.md

ポイントは、MLflowのメタデータと成果物をローカルに置いていることです。

mlflow.db    : MLflowの実験情報
mlartifacts/ : 図、モデル、CSVなどの成果物
outputs/     : 通常の出力ファイル
DuckDB       : 詳細な結果保存

MLflowサーバーを起動する

ローカルサーバーとしてMLflowを起動します。

cd ~/exp_management_local
source .venv/bin/activate

mlflow server \
  --backend-store-uri sqlite:///mlflow.db \
  --default-artifact-root ./mlartifacts \
  --host 0.0.0.0 \
  --port 5000

同じサーバー内から見る場合は、次で開けます。

http://127.0.0.1:5000

別PCから見る場合は、Ubuntu ServerのIPアドレスを使います。

http://<Ubuntu_ServerのIPアドレス>:5000

ファイアウォールを使っている場合は、必要に応じてポートを開けます。

sudo ufw allow 5000/tcp

ただし、社内LANや自宅LANで使う場合でも、外部に公開しないように注意します。


YAMLで計算条件を書く

設定ファイルはYAMLにしておくと、後で見返しやすいです。

configs/base.yaml の例です。

project:
  name: sample_experiment
  experiment_name: local_experiment_management

data:
  input_path: data/raw/input.csv
  output_db: outputs/duckdb/results.duckdb

params:
  case_name: case_001
  alpha: 0.5
  beta: 1.2
  max_iter: 100
  seed: 42

mlflow:
  tracking_uri: http://127.0.0.1:5000

このようにしておくと、「どの条件で計算したか」をファイルとして残せます。


MLflowとDuckDBに記録するサンプル

簡単なサンプルです。

src/run_experiment.py を作ります。

"""MLflowとDuckDBを使ったローカル実験管理の最小サンプル。

このスクリプトでは、説明用の簡単な反復計算を実行し、
実験条件と代表指標をMLflowへ、詳細な計算結果をDuckDBへ保存します。

実際の業務では、dummy_calculation() の中身を、
機械学習、数値シミュレーション、統計処理などの本処理に置き換えます。
"""

from __future__ import annotations

import time
from pathlib import Path
from typing import Any

import duckdb
import matplotlib.pyplot as plt
import mlflow
import pandas as pd
import yaml


CONFIG_PATH = Path("configs/base.yaml")
FIGURE_DIR = Path("outputs/figures")
DUCKDB_DIR = Path("outputs/duckdb")


def load_config(path: Path) -> dict[str, Any]:
    """YAML設定ファイルを読み込む。

    Parameters
    ----------
    path : Path
        読み込むYAMLファイルのパス。
        この記事の例では ``configs/base.yaml`` を指定する。

    Returns
    -------
    dict[str, Any]
        YAMLの内容をPythonの辞書として返す。
        ``project``、``data``、``params``、``mlflow`` などのキーを想定する。

    Raises
    ------
    FileNotFoundError
        指定したYAMLファイルが存在しない場合に発生する。
    yaml.YAMLError
        YAMLの書式が壊れている場合に発生する。
    """
    with path.open("r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def prepare_output_dirs() -> None:
    """出力用ディレクトリを作成する。

    ``exist_ok=True`` にしているため、すでにディレクトリがある場合でも
    エラーにせず、そのまま処理を続ける。
    """
    FIGURE_DIR.mkdir(parents=True, exist_ok=True)
    DUCKDB_DIR.mkdir(parents=True, exist_ok=True)


def dummy_calculation(alpha: float, beta: float, max_iter: int) -> pd.DataFrame:
    """説明用の簡単な反復計算を実行する。

    計算式は以下の通り。

    ``value_next = alpha * value_current + beta``

    ここでは、パラメータを変えて何度も同じ計算を実行する状況を
    わかりやすく示すため、あえて単純な式にしている。

    Parameters
    ----------
    alpha : float
        前ステップの値に掛ける係数。
    beta : float
        各ステップで加算する値。
    max_iter : int
        反復回数。

    Returns
    -------
    pd.DataFrame
        ``step`` と ``value`` を持つ計算結果テーブル。
        DuckDBへ保存しやすいようにDataFrameで返す。
    """
    rows: list[dict[str, float | int]] = []
    value = 1.0

    for step in range(max_iter):
        # ここが計算本体。
        # 実際のモデルでは、この部分がシミュレーションや推論処理になる。
        value = alpha * value + beta

        # 後で時系列として確認できるように、各ステップの値を保存する。
        rows.append({"step": step, "value": value})

    return pd.DataFrame(rows)


def log_parameters_to_mlflow(params: dict[str, Any]) -> None:
    """実験パラメータをMLflowへ記録する。

    Parameters
    ----------
    params : dict[str, Any]
        YAMLの ``params`` セクション。

    Notes
    -----
    MLflowの ``log_param`` は、後からrunを比較するときの軸になる。
    そのため、計算結果に影響する条件はできるだけ記録しておく。
    """
    mlflow.log_param("case_name", params["case_name"])
    mlflow.log_param("alpha", params["alpha"])
    mlflow.log_param("beta", params["beta"])
    mlflow.log_param("max_iter", params["max_iter"])
    mlflow.log_param("seed", params["seed"])


def save_result_plot(df: pd.DataFrame, case_name: str) -> Path:
    """計算結果の折れ線グラフを作成してPNGとして保存する。

    Parameters
    ----------
    df : pd.DataFrame
        ``step`` と ``value`` を含む計算結果。
    case_name : str
        図のタイトルに使うケース名。

    Returns
    -------
    Path
        保存したPNGファイルのパス。
    """
    fig_path = FIGURE_DIR / "result_curve.png"

    plt.figure(figsize=(8, 4))
    plt.plot(df["step"], df["value"])
    plt.xlabel("step")
    plt.ylabel("value")
    plt.title(case_name)
    plt.tight_layout()
    plt.savefig(fig_path, dpi=150)
    plt.close()

    return fig_path


def save_results_to_duckdb(
    df: pd.DataFrame,
    db_path: str,
    run_id: str,
    case_name: str,
) -> None:
    """詳細な計算結果をDuckDBへ保存する。

    Parameters
    ----------
    df : pd.DataFrame
        計算結果のDataFrame。
    db_path : str
        保存先DuckDBファイルのパス。
    run_id : str
        MLflowが発行したrun ID。
        DuckDB側にも保存しておくと、MLflowのrunと詳細結果を対応づけられる。
    case_name : str
        実験ケース名。

    Notes
    -----
    MLflowには代表指標を保存し、DuckDBには詳細テーブルを保存する。
    この分担にしておくと、実験数や結果データが増えても管理しやすい。
    """
    result_df = df.copy()
    result_df["run_id"] = run_id
    result_df["case_name"] = case_name

    con = duckdb.connect(db_path)
    try:
        # 初回実行時だけテーブルを作成する。
        # 2回目以降は既存テーブルへ追記する。
        con.execute(
            """
            CREATE TABLE IF NOT EXISTS results (
                run_id TEXT,
                case_name TEXT,
                step INTEGER,
                value DOUBLE
            )
            """
        )

        # DataFrameをDuckDBに一時ビューとして登録する。
        # その後、SQLで必要な列だけをresultsテーブルへ追加する。
        con.register("df_result", result_df)
        con.execute(
            """
            INSERT INTO results
            SELECT run_id, case_name, step, value
            FROM df_result
            """
        )
    finally:
        con.close()


def main() -> None:
    """実験の一連の流れを実行する。

    実行内容は以下の通り。

    1. YAML設定ファイルを読み込む
    2. 出力ディレクトリを準備する
    3. MLflowの接続先と実験名を設定する
    4. MLflow runを開始する
    5. パラメータをMLflowへ記録する
    6. 計算を実行する
    7. 代表指標をMLflowへ記録する
    8. 図と設定ファイルをMLflow artifactとして保存する
    9. 詳細結果をDuckDBへ保存する
    """
    cfg = load_config(CONFIG_PATH)
    prepare_output_dirs()

    # MLflow Tracking Serverの接続先を設定する。
    # ローカルUbuntu Serverで起動したMLflowサーバーを想定している。
    mlflow.set_tracking_uri(cfg["mlflow"]["tracking_uri"])
    mlflow.set_experiment(cfg["project"]["experiment_name"])

    params = cfg["params"]
    start_time = time.time()

    # 1回の実行を1つのrunとして記録する。
    # run_nameには、後から一覧で見やすいようにケース名を入れる。
    with mlflow.start_run(run_name=params["case_name"]) as run:
        run_id = run.info.run_id

        log_parameters_to_mlflow(params)

        df = dummy_calculation(
            alpha=float(params["alpha"]),
            beta=float(params["beta"]),
            max_iter=int(params["max_iter"]),
        )

        # 代表的な評価指標を計算する。
        # ここでは説明用に、最終値と実行時間だけを記録する。
        final_value = float(df["value"].iloc[-1])
        runtime_sec = time.time() - start_time

        mlflow.log_metric("final_value", final_value)
        mlflow.log_metric("runtime_sec", runtime_sec)

        fig_path = save_result_plot(df, params["case_name"])

        # 設定ファイルと図をartifactとして保存する。
        # artifactに入れておくと、MLflow UIから後で確認できる。
        mlflow.log_artifact(str(CONFIG_PATH), artifact_path="config")
        mlflow.log_artifact(str(fig_path), artifact_path="figures")

        save_results_to_duckdb(
            df=df,
            db_path=cfg["data"]["output_db"],
            run_id=run_id,
            case_name=params["case_name"],
        )

        print(f"run_id = {run_id}")
        print(f"final_value = {final_value}")
        print(f"runtime_sec = {runtime_sec:.3f}")


if __name__ == "__main__":
    main()

実行します。

python src/run_experiment.py

MLflow UIを見ると、パラメータや評価指標が記録されています。


DuckDBの中身を確認する

Pythonから確認する場合です。

"""DuckDBに保存した計算結果を確認する簡単なスクリプト。"""

from pathlib import Path

import duckdb


DB_PATH = Path("outputs/duckdb/results.duckdb")


def show_results(limit: int = 10) -> None:
    """DuckDBのresultsテーブルから先頭行を表示する。

    Parameters
    ----------
    limit : int, optional
        表示する行数。デフォルトは10行。
    """
    con = duckdb.connect(str(DB_PATH))
    try:
        # fetchdf()を使うと、SQLの結果をpandas.DataFrameとして取得できる。
        result_df = con.execute(
            "SELECT * FROM results LIMIT ?",
            [limit],
        ).fetchdf()
        print(result_df)
    finally:
        con.close()


if __name__ == "__main__":
    show_results(limit=10)

SQLで集計できるので、ケース数が増えたときに便利です。

SELECT
    case_name,
    MAX(value) AS max_value,
    MIN(value) AS min_value
FROM results
GROUP BY case_name;

Hydraを使う場面

ケース数が少ないうちは、YAMLを手で書き換えても問題ありません。

ただ、次のような組み合わせを試したくなると、手作業では面倒です。

alpha = 0.3, 0.5, 0.7
beta  = 1.0, 1.2, 1.5
max_iter = 100, 200

このような全組み合わせ実行にはHydraが向いています。

Hydraを使うと、コマンドラインから設定値を上書きできます。

python src/run_experiment.py params.alpha=0.7 params.beta=1.5

さらに、multi-runを使うと複数条件をまとめて流せます。

python src/run_experiment.py -m params.alpha=0.3,0.5,0.7 params.beta=1.0,1.2,1.5

実験数が増えてきた段階で導入すると効果があります。


Optunaを使う場面

Hydraは、指定した組み合わせを全部試すためのものです。

一方で、Optunaは「どの値が良さそうか」を自動で探すために使います。

たとえば、ある評価指標を最小化したい場合は、次のような考え方になります。

"""Optunaで説明用のパラメータ探索を行うサンプル。"""

import optuna


def objective(trial: optuna.Trial) -> float:
    """Optunaが最小化する目的関数。

    Parameters
    ----------
    trial : optuna.Trial
        Optunaが1回の試行ごとに渡してくるTrialオブジェクト。
        このオブジェクトを使って探索するパラメータを定義する。

    Returns
    -------
    float
        最小化したいスコア。
        この例では、alpha=0.6、beta=1.3に近いほど小さくなる値を返す。

    Notes
    -----
    実際の用途では、この関数の中でモデル計算やシミュレーションを実行し、
    RMSE、誤差、計算コストなどの評価値を返す。
    """
    # suggest_float()で、探索する連続値パラメータの範囲を指定する。
    alpha = trial.suggest_float("alpha", 0.1, 0.9)
    beta = trial.suggest_float("beta", 0.5, 2.0)

    # ここでは説明用に単純な二次関数を評価指標にしている。
    # scoreが小さいほど良い、という想定。
    score = (alpha - 0.6) ** 2 + (beta - 1.3) ** 2
    return score


def main() -> None:
    """Optunaのstudyを作成し、50回の探索を実行する。"""
    study = optuna.create_study(direction="minimize")
    study.optimize(objective, n_trials=50)

    print("best_params:", study.best_params)
    print("best_value:", study.best_value)


if __name__ == "__main__":
    main()

機械学習のハイパーパラメータ探索だけでなく、シミュレーションの係数調整にも使えます。


Prefectを使う場面

処理が1本のスクリプトで済むうちは、Prefectは不要です。

ただし、次のようにステップが増えてくると、ワークフローとして管理したくなります。

1. データ取得
2. 前処理
3. 計算実行
4. 結果集計
5. 図作成
6. MLflowへ記録
7. DuckDBへ保存
8. レポート作成

このような場合にPrefectを使います。

簡単な例です。

"""Prefectで複数ステップの処理をワークフロー化する最小例。"""

from prefect import flow, task


@task
def preprocess() -> None:
    """前処理ステップ。

    実際の処理では、入力データの取得、欠損補完、単位変換、
    学習用データや計算用データの作成などを行う。
    """
    print("preprocess")


@task
def run_model() -> None:
    """計算または学習を実行するステップ。

    数値シミュレーション、機械学習モデルの学習、推論処理など、
    プロジェクトの中心になる処理をここに書く。
    """
    print("run model")


@task
def summarize() -> None:
    """結果を集計するステップ。

    評価指標の計算、図の作成、MLflowへの記録、
    DuckDBへの保存などをここで行う。
    """
    print("summarize")


@flow(name="sample-local-workflow")
def main_flow() -> None:
    """3つのtaskを順番に実行するワークフロー。"""
    preprocess()
    run_model()
    summarize()


if __name__ == "__main__":
    main_flow()

最初から入れるというより、定期実行、再実行、失敗時のリトライなどが必要になった段階で使うのがよいです。


DVCを使う場面

Gitだけでは、大きな入力データやモデルファイルの管理がつらくなります。

たとえば、次のようなものです。

data/raw/input.csv
outputs/model/model.pkl
outputs/predictions/result.parquet

このようなファイルをバージョン管理したい場合にDVCを使います。

まずGitとDVCを初期化します。

git init
dvc init

入力データをDVC管理にします。

dvc add data/raw

Gitには、DVCが作ったメタファイルを登録します。

git add data/raw.dvc .gitignore
git commit -m "add raw data with dvc"

ローカルの別ディレクトリをDVC remoteにすることもできます。

mkdir -p ~/dvc_storage
dvc remote add -d localstore ~/dvc_storage

データを保存します。

dvc push

DVCを使うと、コードだけでなく、入力データや成果物も含めて「どの状態で計算したか」を残しやすくなります。


導入順のおすすめ

個人的には、次の順番が扱いやすいです。

段階 入れるもの 目的
1 YAML + MLflow + DuckDB 実験条件と結果を残す
2 Hydra ケースをまとめて実行する
3 Optuna パラメータを自動探索する
4 DVC 入力データと成果物を版管理する
5 Prefect 処理全体をワークフロー化する

最初から全部入れると、環境構築だけで疲れます。

まずは、MLflowで実験を残す。詳細結果はDuckDBに入れる。設定はYAMLで残す。これだけでも、だいぶ管理しやすくなります。


使い分けのまとめ

最後に、使い分け案をもう一度まとめます。
あくまでも、個人の見解です。

実験管理環境の使い分け案

  • MLflow
    実験条件、評価指標、図、モデル、ログを管理する

  • DuckDB
    大量の表形式データ、時系列データ、計算結果を保存する

  • Hydra
    YAML設定を切り替えながら複数ケースを実行する

  • Optuna
    評価指標が良くなるパラメータを自動で探す

  • Prefect
    複数ステップの処理をワークフローとして管理する

  • DVC
    入力データ、加工データ、成果物をバージョン管理する

ローカルのUbuntu Serverで始めるなら、まずは次の構成で十分だと考えています。

Ubuntu Server
  ├─ Python venv
  ├─ MLflow server
  ├─ SQLite backend
  ├─ local artifact store
  ├─ DuckDB
  └─ YAML設定ファイル

ここから始めて、必要になったらHydra、Optuna、DVC、Prefectを足していくのが現実的ですね。


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?