JijModelingで定義した最適化問題をPRINTEMPSで解く
この記事はJij Inc. Advent Calendar 2024の23日目の記事です。
はじめに
最近の業務でSCIP(フリーのMIPソルバー)では求解に時間がかかり過ぎる状況に直面し、OSSのメタヒューリスティクスILPソルバーであるPRINTEMPSを利用させて頂く機会がありました。Jijの業務ではモデラーとして弊社が提供するJijModelingを利用しており、本記事では簡単なナップサック問題を例にJijModelingで定義した最適化問題をPRINTEMPSで解く流れを紹介します。本記事で利用するツールは全てOSSもしくは無償のものなので、ご興味を持たれた方はぜひ試してみてください。
PRINTEMPSについて
PRINTEMPSは冒頭でも触れたように、OSSのメタヒューリスティクスILPソルバーです。私はOptimization Nightで知りました。そちらでのスライド資料も置いておきます。分枝限定法ベースのMIPソルバーが苦手なケースを補完できることを目指したソルバーとのことで今回利用させて頂きました。開発者の方、ありがとうございます。
本題
以下、ナップサック問題をJijModelingで定義し、PRINTEMPSで解いていきます。
Python環境の準備
この記事では以下を使用します。
jijmodeling == 1.10.1
ommx == 1.5.1
ommx-pyscipopt-adapter == 1.5.1
$ pip install -r requirements.txt
JijModelingによる定式化
まずJijModelingでナップサック問題を定式化します。
import jijmodeling as jm
# 定数の定義
v = jm.Placeholder('v', ndim=1, description="value of item")
w = jm.Placeholder('w', ndim=1, description="weight of item")
W = jm.Placeholder('W', description="capacity of knapsack")
N = v.len_at(0, latex="N", description="number of items")
# 決定変数の定義
x = jm.BinaryVar('x', shape=(N,))
# 添字の定義
i = jm.Element('i', belong_to = (0, N))
# 問題の定義
problem = jm.Problem('Knapsack')
problem += - jm.sum(i, v[i] * x[i])
problem += jm.Constraint('weight', jm.sum(i, w[i] * x[i]) <= W)
problem
出力
JijModelingでは数式に近い書き方でモデリングを行うことができ、また画像のように定義した数理モデルをLaTeX表示する機能があります。ナップサック問題はアイテムの重さの合計が
インスタンスデータの準備
以下のようにインスタンスデータを定義しました。対応するjm.Placeholderに値が代入されます。
price = [5000, 7000, 2000, 1000, 4000, 3000]
weight = [800, 1000, 600, 400, 500, 300]
capacity = 2000
instance_data = {'v': price, 'w':weight, 'W': capacity}
SCIPによる求解
まずは厳密ソルバーであるSCIPで今回の問題の最適解を取得しておきます。
JijModelingで定義した問題をSCIPで解くためにJijが提供するOMMXというソフトウェアを利用します。OMMXは数理最適化向けのデータフォーマットであり、OMMXを経由してJijModelingで定義した問題を各種のソルバーで求解することができます。後段でPRINTEMPSでの求解を実施する時もOMMXを利用します。OMMXについてはこちらのリリース記事もご確認ください。
以下、OMMXを利用してSCIPでの求解を行うコードです。
from ommx_pyscipopt_adapter import instance_to_model, model_to_solution
# problem、instance_dataからOMMXのインスタンスを生成
interpreter = jm.Interpreter(instance_data)
ommx_instance = interpreter.eval_problem(problem)
# OMMXのインスタンスからSCIPのモデルを生成
model = instance_to_model(ommx_instance)
# SCIPで最適化
model.optimize()
# SCIPの解をOMMXのSolutionに変換
ommx_solution_by_scip = model_to_solution(model, ommx_instance)
# 結果の表示
display(ommx_solution_by_scip.decision_variables)
print(f'SCIPで求解した時の目的関数値: {ommx_solution_by_scip.objective}')
出力
今回のナップサック問題の最適解は-14,000と確認できました。また決定変数
PRINTEMPSによる求解
次に同じ問題をPRINTEMPSで解いていきます。
OMMXを利用したmpsファイルへの書き出し
mpsファイルで最適化問題をPRINTEMPSに与えるので、JijModelingで定義した問題をmpsファイルに出力します。
ommx_instance.write_mps(f'knapsack.mps.gz')
$ gzip -d -k knapsack.mps.gz
knapsack.mps
というファイルが生成されます。
PRINTEMPSの準備
次にPRINTEMPSを準備していきます。PRINTEMPSの公式ドキュメントはこちらです。こちらの記事も参考になります。これらの内容に従い、スタンドアローンソルバーを準備します。以下に私が実施した手順をまとめておきます。
$ git clone https://github.com/snowberryfield/printemps.git
$ cd printemps # クローンしてきたリポジトリに移動
$ make -f makefile/Makefile.application
これによりprintemps/build/application/Release/mps_solver
という実行可能ファイルが生成されると思います。
PRINTEMPSによる求解
以下のコマンドで求解が実行できます。
$ printemps/build/application/Release/mps_solver knapsack.mps -p printemps/application/dat/option.json
printemps/application/dat/option.json
はPRINTEMPSソルバーの設定ファイルですが、今回の問題はデフォルト設定のままで問題ないかと思います。求解が完了すると以下の3つのファイルが生成されます。
incumbent.json
incumbent.sol
status.json
incumbent.jsonを確認してみると目的関数値は-14,000でSCIPの結果と一致しており、無事PRINTEMPSで求解することが出来ました。
{
"version": "v2.5.0",
"name": "knapsack",
"number_of_variables": 6,
"number_of_constraints": 1,
"is_found_feasible_solution": true,
"objective": -1.400000e+04,
"total_violation": 0.000000e+00,
"variables": {
"OMMX_VAR_0": 0,
"OMMX_VAR_1": 1,
"OMMX_VAR_2": 0,
"OMMX_VAR_3": 0,
"OMMX_VAR_4": 1,
"OMMX_VAR_5": 1
},
"expressions": {},
"constraints": {
"OMMX_CONSTR_0": -2.000000e+02
},
"violations": {
"OMMX_CONSTR_0": 0.000000e+00
}
}
OMMXのsolution形式への変換
JijModelingで定義した問題をPRINTEMPSで解くという本記事の目的は既に達成されました。しかし実際に数理最適化問題に取り組んでいる時はSCIPの結果とPRINTEMPSの結果を比較することになるので、双方の結果が同じ形式になっていた方が便利です。PRINTEMPSの結果について以下のようにしてOMMXのSolutionに変換することができます。
import json
printemps_solution_path = 'incumbent.json'
with open(printemps_solution_path, 'r') as f:
printemps_solution = json.load(f)
ommx_solution_by_printemps = ommx_instance.evaluate({
var.id: printemps_solution['variables'][f'OMMX_VAR_{var.id}']
for var in ommx_instance.raw.decision_variables
})
display(ommx_solution_by_printemps.decision_variables)
print(f'PRINTEMPSで求解した時の目的関数値: {ommx_solution_by_printemps.objective}')
出力
SCIPによる最適解と同様の結果が得られていることが確認できます。
最後に
Jijでは数理最適化・量子コンピュータに関連するソフトウェア開発を行っています。
OSSも複数開発、提供しています。Discordのコミュニティへぜひ参加してください!
また,Jijでは各ポジションを絶賛採用中です!
ご興味ございましたらカジュアル面談からでもぜひ!ご連絡お待ちしております。
数理最適化エンジニア(採用情報)
量子コンピューティングソフトウェアエンジニア(採用情報)
Rustエンジニア(アルゴリズムエンジニア)(採用情報)
研究チームPMO(採用情報)
オープンポジション等その他の採用情報はこちら
Discussion