Python

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

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のペナルティ係数探索、移行点、採用を見送る条件を説明します。定式化の基礎は決定変数・目的関数・制約条件の解説、数理最適化の全体像は数理最適化の入門記事を参照してください。

まとめ: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は、mintoを機械学習分野のmlflowに近い位置づけと説明し、結果の保存、ベンチマーク指標の自動計算、可視化の補助を機能に挙げています。MLflowとの違いは、OMMXの問題・解オブジェクトを受け取る記録APIを備えている点です。log_solutionにSolutionを渡すだけで、目的関数値や実行可能性が表に展開されます。

OMMX(Open Mathematical prograMming eXchange)は、同じくJijが開発している最適化問題と解の共通データ形式です。ソルバーごとのアダプタが、固有の結果をSolutionやSampleSetに変換します。ソルバーの選び方は主要ORツールの比較にまとめています。

PyPIの説明文には「主にIsing問題が対象」「チュートリアルは今後提供」と書かれていますが、古い記述です。公式のQuick Startは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の使い方を参照してください。PyPIの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の最新安定版は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の定義で0が未指定、1が最適、2が非最適を表します。後述の時間切れの試行はすべて0でした。0は「最適ではない」ではなく、最適性を判定できていないという意味です。もう1つのnum_consは、ommx 2.9.4の組み合わせでは制約があってもNoneでした。参照元のInstance.num_constraintsがNoneを返すためで、制約数が要るならlen(instance.constraints)で数えます。

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

mintoの中心の用途で、公式の例も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を記録します。温度パラメータを振る例は公式のユースケースにあります。

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の提案値を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とModel Registryが担います。
  • 長期の保守を前提にする:公開リポジトリは配布用のミラーで、2026年11月9日時点でissueは無効、コミットは1件、最終更新は2026年4月13日です。変更の経緯を公開情報から追えないため、依存ライブラリの更新や不具合調査を自社で引き受けられるかを先に確認します。

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

よくある質問

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で残します。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法
  2. 2026.11.11 テックブログ JANISの不正アクセス:最大11,652名の医療機関情報と情報窃取プログラムの検知設定
  3. 2026.11.11 テックブログ SMBC日興証券の不正アクセス:持株会会員最大5.6万人の範囲と受託システムの分離設計
  4. 2026.01.21 コラム 税務調査の録音は禁止?判例・裁決と国税局マニュアルで見る、断られたときの対応
  5. 2026.11.09 テックブログ ステップゴルフ不正アクセス:口座情報の対象確認と偽連絡・引き落としの注意点

RELATED POSTS 関連記事

目次