---
title: "MINTOとは？数理最適化の実験管理をPythonで記録する使い方【minto 2.0対応】"
url: "https://www.issoh.co.jp/tech/details/10145/"
published: 2025-12-05
updated: 2026-11-09
categories: ["Python"]
publisher: "株式会社一創"
---

# MINTOとは？数理最適化の実験管理をPythonで記録する使い方【minto 2.0対応】

MINTO（Pythonパッケージ名は`minto`）は、株式会社Jijが公開している数理最適化向けの実験管理ライブラリです。ソルバーに渡したパラメータ、問題インスタンス、得られた解を「実験（Experiment）」と「試行（Run）」の2層で記録し、あとから表で比較・共有できます。この記事のコードは、minto 2.0.0、ommx 2.9.4、jijmodeling 2.11.0（2026年11月9日時点のPyPI最新安定版）で実行したものです。記録の仕組みに加え、ソルバーと制限時間の比較、QUBOのペナルティ係数探索、移行点、採用を見送る条件を説明します。定式化の基礎は[決定変数・目的関数・制約条件の解説](/tech/details/8321/)、数理最適化の全体像は[数理最適化の入門記事](/tech/details/2976/)を参照してください。

## まとめ：minto 2.0で押さえる要点

- mintoはJijが開発するMITライセンスのPythonライブラリで、数理最適化の実験結果を記録・比較・共有する。機械学習におけるMLflowに近い位置づけ
- 記録は`Experiment`（全試行で共通）と`Run`（試行ごと）の2層。`with experiment.run() as run:`の中で`run.log_parameter`や`run.log_solution`を呼ぶ
- 解はOMMX形式で受け取るため、HiGHS・SCIP・OpenJijを同じコードで記録できる
- 2.0.0の要件はPython 3.11以上3.14未満、jijmodeling 2.3.2以上、ommx 2.x（3.0はベータのみ）。1.xの`Experiment.log_parameter`など実験直下の記録メソッドは削除された
- Intel Macではommxのwheelが無く、`pip install minto`はそのままでは通らない
- 表の読み違いに注意：`optimality`が0なら最適性は未確定、サンプルの`obj_min`は実行不可能な解も含む

## MINTOの役割：数理最適化の実験条件と解の記録

最適化の数値実験では、同じ問題をソルバーの設定（時間制限、温度パラメータ、ペナルティ係数など）だけ変えて何十回も解きます。記録を手作業のCSVやノートブックの出力に頼ると、どの設定でその目的関数値が出たのか、当時のライブラリの版は何だったのかが後から追えなくなります。mintoはこの記録を定型化します。

[公式リポジトリのREADME](https://github.com/Jij-Inc/MINTO-Public)は、mintoを機械学習分野のmlflowに近い位置づけと説明し、結果の保存、ベンチマーク指標の自動計算、可視化の補助を機能に挙げています。MLflowとの違いは、OMMXの問題・解オブジェクトを受け取る記録APIを備えている点です。`log_solution`に`Solution`を渡すだけで、目的関数値や実行可能性が表に展開されます。

OMMX（Open Mathematical prograMming eXchange）は、同じくJijが開発している最適化問題と解の共通データ形式です。[ソルバーごとのアダプタ](https://jij-inc-ommx.readthedocs-hosted.com/en/latest/api/adapters.html)が、固有の結果を`Solution`や`SampleSet`に変換します。ソルバーの選び方は[主要ORツールの比較](/tech/details/3034/)にまとめています。

PyPIの説明文には「主にIsing問題が対象」「チュートリアルは今後提供」と書かれていますが、古い記述です。[公式のQuick Start](https://jij-inc-minto.readthedocs-hosted.com/latest/en/quickstart)はSCIPでMIPLIBの問題を解く例を載せており、英語版と日本語版のドキュメントがあります。後述の実行例でも、MIPソルバーとアニーリングを同じAPIで記録しています。

### 同じ名前の別ソフト「MINTO（Mixed INTeger Optimizer）」との区別

「MINTO 最適化」で調べると、1990年代にG.L. Nemhauser、M.W.P. Savelsberghらが開発した混合整数計画ソルバー「MINTO（Mixed INTeger Optimizer）」の資料も出てきます。こちらは分枝限定法で問題を解くソルバー本体で、Jijのmintoはソルバーの実行結果を記録する側です。探すときは「minto Jij」「minto ommx」と組み合わせると取り違えません。

## ExperimentとRunの2層構造：共通データと試行データの記録先

問題インスタンスのように全試行で共通のものは`Experiment`に1回だけ記録し、試行ごとに変わるパラメータや解は`Run`に記録します。主なメソッドは次のとおりです。

| 記録するもの           | Experiment側（全Run共通）                          | Run側（試行ごと）                   |
| ---------------- | -------------------------------------------- | ---------------------------- |
| スカラーのパラメータ       | log\_global\_parameter / log\_global\_params | log\_parameter / log\_params |
| jijmodelingの問題定義 | log\_global\_problem                         | log\_problem                 |
| OMMXのInstance    | log\_global\_instance                        | log\_instance                |
| 辞書形式の任意データ       | log\_global\_config / log\_global\_object    | log\_object                  |
| OMMXのSolution    | なし                                           | log\_solution                |
| OMMXのSampleSet   | なし                                           | log\_sampleset               |
| ソルバー関数の呼び出し      | なし                                           | log\_solver                  |

`log_parameter`が受け付けるのはint・float・strと0次元のnumpy配列だけです。リストや1次元以上の配列は`TypeError`になるので、`log_object`に辞書で入れます。記録した値は`exp.runs[0].parameters`や`exp.runs[0].solutions["highs"]`のように`DataStore`から取り出せます。

### withで閉じたRunのelapsed\_timeと、例外時の残り方

`with`句を抜けると`close()`が呼ばれ、`elapsed_time`がメタデータに入ります。閉じた`Run`に記録すると`RuntimeError`です。この値はRunを作ってから閉じるまでの時間で、ソルバー単体の時間ではありません。インスタンスの構築や保存が重い場合は、ソルバーの実行時間を別のパラメータとして記録します。

試行の途中で例外が起きても、その試行は消えません。3試行のうち2回目で`ValueError`を投げて止めると、`runs/0`と`runs/1`が残り、`load_from_dir`で読み戻すと2試行分の行が出ました。困るのは、失敗を示す列が無いことです。成功・失敗を区別するため、`with`句の内側で例外を捕捉し、`except`節で`run.log_parameter("status", "failed")`のように自分で印を残します。

### log\_solverで引数と戻り値を自動記録する条件

`run.log_solver(関数)`は、渡した関数を包んだ関数を返します。呼び出すと関数名が`solver_name`に入り、キーワード引数のうちint・float・strはパラメータ、jijmodelingの`Problem`は問題、OMMXの`Instance`はインスタンスとして記録されます。戻り値がOMMXの`Solution`または`SampleSet`の場合に限り「関数名\_result」という名前で保存され、`highs_solve`を包むと列は`solution_highs_solve_result`になりました。位置引数は記録されないので、値は必ずキーワード引数で渡します。乱数シードは再現に関わるため、`exclude_params`で除外しません。

## インストールと動作環境：minto 2.0.0の要件

インストールは`pip install minto`です。ソルバーは別に入れます。HiGHSなら`ommx-highs-adapter`、SCIPなら`ommx-pyscipopt-adapter`、OpenJijなら`ommx-openjij-adapter`です。SCIP側の手順は[PySCIPOptの使い方](/tech/details/3037/)を参照してください。[PyPIのmintoの配布ページ](https://pypi.org/project/minto/)に登録された要件は次のとおりです。

| 項目          | minto 1.4.1（2025-11-03） | minto 2.0.0（2026-04-13） |
| ----------- | ----------------------- | ----------------------- |
| Python      | 3.10以上3.13未満            | 3.11以上3.14未満            |
| jijmodeling | 1.12.4以上2.0未満           | 2.3.2以上3.0未満            |
| ommx        | 2.0以上3.0未満              | 2.0以上3.0未満              |
| numpy       | 1.24.4以上3.0未満           | 2.0以上3.0未満              |
| pandas      | 1.5.3以上3.0未満            | 2.1.2以上3.0未満            |

2026年11月9日時点で、[PyPIのommx](https://pypi.org/project/ommx/)の最新安定版は2.9.4で、3.0はベータ（3.0.0b9）のみです。minto 2.0.0の要件は`ommx<3.0.0`なので、ベータ版は入りません。

問題になるのは配布形式です。ommx 2.9.4のwheelはmacOS（Apple Silicon）、Linux（x86\_64・aarch64）、Windows（x64）向けだけで、ソース配布物もありません。Intel Macでは`pip install minto`が依存解決で止まります。この記事の実行環境はIntel Macなので、GitHubのタグ`python-2.9.4`からRustツールチェーンでommxをビルドしました。チームで環境を揃えるなら、LinuxのコンテナかApple Silicon機を使う方が手間がかかりません。jijmodelingとOpenJij、highspyにはIntel Mac向けのwheelがあります。

## 容量を変えてナップサック問題を解く記録コード例

6品目のナップサック問題を容量8・12・16・20で解き、各試行を記録します。Python 3.12で実行しました。

```
import minto
from ommx.v1 import DecisionVariable, Instance
from ommx_highs_adapter import OMMXHighsAdapter

values = [10, 13, 7, 8, 11, 6]
weights = [5, 6, 3, 4, 5, 2]

def build(capacity):
    x = [DecisionVariable.binary(i) for i in range(len(values))]
    return Instance.from_components(
        decision_variables=x,
        objective=sum(v * xi for v, xi in zip(values, x)),
        constraints=[sum(w * xi for w, xi in zip(weights, x)) <= capacity],
        sense=Instance.MAXIMIZE,
    )

exp = minto.Experiment("knapsack_capacity", verbose_logging=False)
exp.log_global_params({"n_items": len(values)})

for capacity in [8, 12, 16, 20]:
    with exp.run() as run:
        run.log_parameter("capacity", capacity)
        instance = build(capacity)
        run.log_instance("knapsack", instance)
        solution = OMMXHighsAdapter.solve(instance)
        run.log_solution("highs", solution)

df = exp.get_run_table()
print(df[[("parameter", "capacity"), ("solution_highs", "objective"), ("solution_highs", "feasible")]])
```

実行結果です。

```
       parameter solution_highs
        capacity      objective feasible
run_id
0              8           19.0     True
1             12           27.0     True
2             16           37.0     True
3             20           45.0     True
```

容量ごとに`Instance`が変わるのでRun側に入れ、全試行で同じ品目数は`log_global_params`で実験側に入れています。既定ではカレントディレクトリの`.minto_experiments`に「実験名\_年月日時分秒」のディレクトリができ、Runごとの記録が`runs/0`、`runs/1`に書き込まれます。

## get\_run\_tableの読み方：2段ヘッダーと読み違えやすい列

`get_run_table()`はRunごとに1行のpandas DataFrameを返します。列は2段ヘッダーで、列指定は`df[("parameter", "capacity")]`のようにタプルで書きます。名前を省いて`log_solution(solution)`と書くと、1段目は`solution_0`になります。グラフは`df.plot(x=("parameter", "capacity"), y=("solution_highs", "objective"))`で描けます。この表は要約なので、決定変数の値は`exp.runs[0].solutions["highs"].decision_variables_df`で見ます。実験側の値は`get_experiment_tables()`が辞書で返します。

読み違えやすいのは次の2列です。`optimality`は[OMMXの定義](https://github.com/Jij-Inc/ommx/blob/main/proto/ommx/v1/solution.proto)で0が未指定、1が最適、2が非最適を表します。後述の時間切れの試行はすべて0でした。0は「最適ではない」ではなく、最適性を判定できていないという意味です。もう1つの`num_cons`は、ommx 2.9.4の組み合わせでは制約があっても`None`でした。参照元の`Instance.num_constraints`が`None`を返すためで、制約数が要るなら`len(instance.constraints)`で数えます。

## ソルバーと制限時間を振る比較実験：HiGHSとSCIPの記録例

mintoの中心の用途で、[公式の例](https://jij-inc-minto.readthedocs-hosted.com/v1/en/usecases/mip-time)もSCIPの制限時間を振っています。ここでは100品目・10制約の多次元ナップサック問題を、2つのソルバーと3通りの制限時間で各3回解きました。

```
import minto
import numpy as np
from ommx.v1 import DecisionVariable, Instance
from ommx_highs_adapter import OMMXHighsAdapter
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter

rng = np.random.default_rng(0)
n, m = 100, 10
w = rng.integers(1, 1000, size=(m, n))
c = (w.sum(axis=0) / m + rng.integers(1, 500, size=n)).round()
cap = (w.sum(axis=1) * 0.5).astype(int)

x = [DecisionVariable.binary(j) for j in range(n)]
instance = Instance.from_components(
    decision_variables=x,
    objective=sum(float(c[j]) * x[j] for j in range(n)),
    constraints=[sum(int(w[i, j]) * x[j] for j in range(n)) <= int(cap[i]) for i in range(m)],
    sense=Instance.MAXIMIZE,
)

def solve_highs(limit):
    adapter = OMMXHighsAdapter(instance)
    model = adapter.solver_input
    model.setOptionValue("time_limit", float(limit))
    model.setOptionValue("output_flag", False)
    model.run()
    return adapter.decode(model)

def solve_scip(limit):
    adapter = OMMXPySCIPOptAdapter(instance)
    model = adapter.solver_input
    model.hideOutput()
    model.setParam("limits/time", float(limit))
    model.optimize()
    return adapter.decode(model)

exp = minto.Experiment("mkp_time_limit", verbose_logging=False, auto_saving=False)
exp.log_global_instance("mkp_100_10", instance)

for repeat in range(3):
    for limit in [1, 5, 20]:
        for name, solve in [("highs", solve_highs), ("scip", solve_scip)]:
            with exp.run() as run:
                run.log_parameter("solver", name)
                run.log_parameter("time_limit", limit)
                run.log_parameter("repeat", repeat)
                run.log_solution("best", solve(limit))

df = exp.get_run_table()
keys = [("parameter", "solver"), ("parameter", "time_limit")]
print(df.groupby(keys)[[("solution_best", "objective")]].median())
```

目的関数値（大きいほど良い）の中央値は次のとおりで、どの試行も実行可能で`optimality`は0でした。

| 制限時間 | HiGHS  | SCIP   |
| ---- | ------ | ------ |
| 1秒   | 42,961 | 43,123 |
| 5秒   | 42,961 | 43,123 |
| 20秒  | 43,077 | 43,123 |

Intel Core i9-9880H上で、highspy 1.15.1、PySCIPOpt 6.3.0、各OMMXアダプタ2.9.4を使い、別の処理と並走させた値です。QUBO例にはOpenJij 0.12.2を使っています。この1問ではSCIPが1秒で43,123に届き、20秒まで更新しませんでした。HiGHSは1秒の試行が42,920から42,961まで揺れ、20秒の中央値も別の計測では43,064でした。時間で打ち切る実験は同じ条件でも結果が変わるので、`repeat`を記録して複数回の中央値で比べます。ソルバーの優劣はこの1問では決まりません。乱数シードを変えたインスタンスを増やし、`log_instance`でRun側に入れて比べてください。

## QUBOのペナルティ係数を振る記録例：OpenJijとminto.problems

アニーリング系では、制約違反に掛けるペナルティ係数の調整が実験の中心になります。`minto.problems`にはTSP・ナップサック・CVRPのベンチマーク問題があり、`eval`で`Instance`にできます。次のコードは6都市のTSPをOpenJijのシミュレーテッドアニーリングで解き、係数を変えて`SampleSet`を記録します。温度パラメータを振る例は[公式のユースケース](https://jij-inc-minto.readthedocs-hosted.com/latest/en/usecases/openjij-temp-tuning/)にあります。

```
import minto
import numpy as np
from minto.problems.tsp import QuadTSP
from ommx_openjij_adapter import OMMXOpenJijSAAdapter

np.random.seed(0)
tsp = QuadTSP()
problem = tsp.problem()
data, _ = tsp.random_data(6)
instance = problem.eval(data)

exp = minto.Experiment("tsp_penalty", verbose_logging=False, auto_saving=False)
exp.log_global_problem("tsp", problem)
exp.log_global_instance("tsp6", instance)

for weight in [0.05, 0.2, 1.0, 5.0]:
    with exp.run() as run:
        run.log_parameter("penalty", weight)
        samples = OMMXOpenJijSAAdapter.sample(
            instance,
            uniform_penalty_weight=weight,
            num_reads=100,
            num_sweeps=1000,
            seed=42,
        )
        run.log_sampleset("sa", samples)

df = exp.get_run_table()
print(df[[("parameter", "penalty"), ("sampleset_sa", "num_samples"),
          ("sampleset_sa", "feasible"), ("sampleset_sa", "obj_min")]].to_string())
```

```
       parameter sampleset_sa
         penalty  num_samples feasible   obj_min
run_id
0           0.05          100        0  0.500000
1           0.20          100        0  1.600000
2           1.00          100      100  1.603791
3           5.00          100      100  1.449582
```

TSPは経路が短いほど良いので、`obj_min`だけを見ると係数0.05の0.50が最良に見えます。しかしこの行の`feasible`は100サンプル中0で、巡回路になっていません。`obj_min`は実行不可能なサンプルも含めた最小値です。実行可能な解を評価するなら`feasible`を先に確かめ、`exp.runs[2].samplesets["sa"].best_feasible`のように取り出します。係数の探索を自動化したい場合は、[Optuna](/tech/details/18671/)の提案値を`log_parameter`で残す組み合わせが素直です。

## 保存と共有の3経路：ディレクトリ・OMMX Archive・GitHub Packages

| 方法              | 保存                                      | 読み込み                                 | 向いている用途    |
| --------------- | --------------------------------------- | ------------------------------------ | ---------- |
| ディレクトリ          | auto\_saving=True（既定）/ save()           | Experiment.load\_from\_dir           | 手元での途中保存   |
| OMMX Archive    | save\_as\_ommx\_archive(“x.minto.ommx”) | Experiment.load\_from\_ommx\_archive | 1ファイルで受け渡し |
| GitHub Packages | push\_github(org, repo)                 | Experiment.load\_from\_registry      | チームでの公開共有  |

`load_from_dir`に渡すのは、タイムスタンプ付きのディレクトリ名です。`exp.savedir`をそのまま渡します。OMMX ArchiveはOCI Artifact形式の1ファイルで、2試行の記録が16KBでした。同名のファイルがあると`RuntimeError: File already exists`で止まるので、上書き保存を前提にしたスクリプトでは先に消すか別名にします。

GitHub Packagesへ送る場合は、`write:packages`権限を持つPersonal Access Token（classic）を環境変数`OMMX_BASIC_AUTH_DOMAIN`（値は`ghcr.io`）・`OMMX_BASIC_AUTH_USERNAME`・`OMMX_BASIC_AUTH_PASSWORD`で渡します。`ommx login`というコマンドもありますが、pipで入るパッケージには含まれず、Rust版のCLIを別に導入する必要があります。`push_github`はタグを省略すると実験開始時刻をタグにします。複数人の実験を1つの表で比べるときは`minto.Experiment.concat([exp_a, exp_b])`で結合します。今回の2実験の結合では`run_id`が0から3に振り直され、結合後の実験名はランダムな文字列になりました。

## minto 1.xから2.0へ移行するときに直す箇所

1.4.1と2.0.0のwheelを比べると、変更点は次の4つです。

- **Experiment直下の記録メソッドの削除**：1.xで非推奨警告付きで残っていた`experiment.log_parameter()`や`experiment.log_solution()`が消え、試行ごとの記録は`Run`、共通データは`log_global_*`に一本化されました。
- **jijmodeling 2系が必須**：1系のモデルは、`problem.Float`や`problem.BinaryVar`で宣言する2系の書き方に直します。
- **Python 3.10の対応終了**：下限が3.11に上がりました。
- **旧APIと移行ツールの削除**：`minto.v0`と`minto.migrator`が取り除かれました。0系の記録が残っているなら、1.4.1の環境で変換を済ませてから上げます。

## MLflow・汎用の実験管理ツールとの使い分け

解をOMMX形式で扱えるなら、mintoを選びます。目的関数値・実行可能性の要約表を自分で組み立てずに済み、ソルバーを替えても記録コードは変わりません。

次の条件に当てはまるなら、MLflowやWeights & Biasesのような汎用ツールを使います。

- **使うソルバーにOMMXアダプタがない**：解を`Solution`に変換できないと、パラメータを記録するだけの道具になります。
- **機械学習の学習ループと同じ基盤で管理したい**：ステップごとの学習曲線、モデル成果物の版管理、Web UIでの比較はmintoにありません。MLflowなら[Tracking UI](https://mlflow.org/docs/latest/ml/tracking/)と[Model Registry](https://mlflow.org/docs/latest/ml/model-registry/)が担います。
- **長期の保守を前提にする**：[公開リポジトリ](https://github.com/Jij-Inc/MINTO-Public)は配布用のミラーで、2026年11月9日時点でissueは無効、コミットは1件、最終更新は2026年4月13日です。変更の経緯を公開情報から追えないため、依存ライブラリの更新や不具合調査を自社で引き受けられるかを先に確認します。

研究段階の記録を業務の最適化システムに組み込む段階では、実行基盤や権限管理の設計が別に要ります。弊社では[フルスクラッチ開発](https://www.issoh.co.jp/service/system/fullscratch/)として個別のシステム構築を承っています。記録項目の一般論は[実験管理の記録項目とrun設計](/tech/details/16340/)、汎用ツール側の比較は[Weights & BiasesとMLflowの使い分け](/tech/details/16077/)で扱っています。

## よくある質問

### mintoは無料で使えますか？

はい。mintoはMITライセンスのオープンソースで、PyPIから無料でインストールできます。依存するommxとjijmodelingも無料で入りますが、組み合わせるソルバーのライセンスは別に確認が必要です。HiGHSやSCIPのようなオープンソースのソルバーなら追加費用なしで試せます。

### mintoはどのソルバーに対応していますか？

mintoは求解アルゴリズムを持たず、OMMXの`Solution`・`SampleSet`を記録します。そのため、SCIP、HiGHS、Python-MIP、OpenJijなどOMMXアダプタがあるソルバーで使えます。公式の一覧にはGurobiやD-Wave、Fixstars Amplify向けも載っています。アダプタがないソルバーでも、`log_parameter`や`log_object`で数値を記録することはできます。

### Intel Macでmintoを使えますか？

PyPIからのインストールだけでは入りません。minto 2.0.0が依存するommxがIntel Mac向けのwheelを配布していないためです。Rustを入れてGitHubのタグからommxをビルドするか、Docker上のLinux環境で使います。この記事のコードはIntel Macでommx 2.9.4をビルドして動かしました。

### 記録した実験結果はどこに保存されますか？

既定ではカレントディレクトリの`.minto_experiments/実験名_年月日時分秒/`です。保存先は`Experiment(savedir="...")`で変えられます。`auto_saving=False`にすると自動保存が止まり、`save()`を呼んだときだけ書き込まれます。

### 実行環境の情報は記録されますか？

はい。`collect_environment=True`（既定）のとき、実験開始時にOS、CPU、メモリ、Pythonのバージョン、主要パッケージのバージョンを記録します。`exp.print_environment_summary()`で表示できます。2.0.0ではminto自身のバージョンが`unknown`になるため、`importlib.metadata.version("minto")`の値を`log_global_parameter`で残します。

## 関連記事

- [数理最適化とは？最適化アルゴリズムの種類と最適化モデルの作り方をPythonで解説](/tech/details/2976/)
- [ORツールとは？OR-Tools・Gurobiなど主要6ツールの比較と選び方【無償版の上限を実測】](/tech/details/3034/)
- [SCIPソルバーとは？PySCIPOptでPythonから使う手順とCBC・HiGHSとの比較【SCIP 10.1】](/tech/details/3037/)
- [実験管理とは？記録項目とrun設計・導入判断を実装目線で解説【2026年版】](/tech/details/16340/)
- [Weights & Biasesとは？実験管理の仕組みとWeave・料金・MLflowとの使い分け【2026年版】](/tech/details/16077/)

---

出典: [MINTOとは？数理最適化の実験管理をPythonで記録する使い方【minto 2.0対応】](<https://www.issoh.co.jp/tech/details/10145/>)（株式会社一創）
