冷めたコーヒー

Weniger, aber besser

混合整数計画問題のソルバー scipy.optimize.milp と Python-MIP でのデフォルトのソルバー CBC の簡単な比較

前回の記事で,SciPy==1.9.0 で新たに導入された scipy.optimize.milp に関する簡単な紹介をしました.

mirucacule.hatenablog.com

本記事では,上記の記事で扱った問題と同様の問題を Pyhton-MIP で求解する方法を紹介します.Python-MIP のより詳細な使用方法については公式ドキュメント [1] を参照してください.

解くべき混合整数計画問題(再掲)

次の混合整数計画問題を考えます:

\begin{align} \min_{\mathbf{x}} \quad &\mathbf{c}^\top\mathbf{x} \\ \text{subject to} \quad&\mathbf{A}\mathbf{x} \leqslant \mathbf{b},\\ &\mathbf{x} \in \mathbb{Z}^n,\,\mathbf{x} \geqslant \mathbf{0}. \end{align}

ここで,$\mathbf{c}\in\mathbb{R}^n, \, \mathbf{A}\in\mathbb{R}^{m\times n}, \, \mathbf{b}\in\mathbb{R}^{m}$ とします.

scipy.optimize.milp による求解(再掲)

必要なパッケージを import しておきます.

import numpy as np
from scipy.optimize import milp
from scipy.optimize import LinearConstraint
from scipy.optimize import Bounds

解くべき問題の係数行列などを定義します.

np.random.seed(42)
n, m = 40, 80
c = np.random.rand(n)
x = np.random.randint(0, 5, n)
A = np.random.randn(m, n)
s = 10 * np.ones_like(m)
b = A @ x + s

制約条件に関わる部分を定義します.

l = np.zeros_like(c)
u = np.full_like(c, np.inf)
b_u = b
b_l = np.full_like(b_u, -np.inf)

bounds = Bounds(l, u)
constraints = LinearConstraint(A, b_l, b_u)
integrality = np.ones_like(c)

求解します.

%%time
res = milp(
    c=c,
    bounds=bounds,
    constraints=constraints,
    integrality=integrality,
)
print(res)

結果は以下の通りです.

            fun: 7.441844504063867
        message: 'Optimization terminated successfully. (HiGHS Status 7: Optimal)'
 mip_dual_bound: 7.44111388697805
        mip_gap: 9.8176881473139e-05
 mip_node_count: 49538
         status: 0
        success: True
              x: array([1., 0., 1., 0., 0., 1., 1., 0., 0., 0., 1., 0., 0., 4., 0., 0., 0.,
       0., 0., 0., 2., 1., 0., 0., 0., 0., 3., 1., 1., 0., 1., 3., 0., 0.,
       1., 0., 0., 1., 0., 0.])
CPU times: user 17.8 s, sys: 665 ms, total: 18.4 s
Wall time: 17.6 s

Python-MIP の CBC による求解

必要なパッケージを import しておきます.

import numpy as np
import mip

同様の問題を扱うため,係数行列などの定義は同様です.

np.random.seed(42)
n, m = 40, 80
c = np.random.rand(n)
x = np.random.randint(0, 5, n)
A = np.random.randn(m, n)
s = 10 * np.ones_like(m)
b = A @ x + s

次に,決定変数,目的関数,制約条件をそれぞれ定義して求解を行います.

model = mip.Model()
# define decision variable
x = {}
for i in range(n):
    x[i] = model.add_var(lb=0, ub=mip.INF, var_type=mip.INTEGER)
# define objective function
model.objective = mip.minimize(mip.xsum(c[i] * x[i] for i in range(n)))
# define constraints
for i in range(m):
    model.add_constr(
        mip.xsum(A[i,:][j] * x[j] for j in range(n)) <= b[i]
    )
# solve
model.optimize()

model.optimize() を実行すると以下のような出力が得られます.実際には,最適化計算の各反復における情報が出力されますが,ここでは省略します.

Cbc0032I Strong branching done 29094 times (142226 iterations), fathomed 305 nodes and fixed 796 variables
Cbc0041I Maximum depth 33, 3997 variables fixed on reduced cost (complete fathoming 1136 times, 157339 nodes taking 667811 iterations)
Total time (CPU seconds):       8.99   (Wallclock seconds):       9.63

<OptimizationStatus.OPTIMAL: 0>

得られた最適値と最適解を確認してみましょう.

print('Objective value: {model.objective_value:.15}'.format(**locals()))
print('Solution: ', end='')
for v in model.vars:
    if v.x > 1e-5:
        print('{v.name} = {v.x}'.format(**locals()))
        print('          ', end='')

実行すると以下のような結果が得られます.

Objective value: 7.44184450406387
Solution: var(0) = 1.0
          var(2) = 1.0
          var(5) = 1.0
          var(6) = 1.0
          var(10) = 1.0
          var(13) = 4.0
          var(20) = 2.0
          var(21) = 1.0
          var(26) = 3.0
          var(27) = 1.0
          var(28) = 1.0
          var(30) = 1.0
          var(31) = 3.0
          var(34) = 1.0
          var(37) = 1.0

最適値,最適解ともに scipy.optimize.milp で計算した結果と整合していることが分かります.なお,求解にかかった時間は CPU 時間だけで比較すると,milp は 17.8 秒,Python-MIP (cbc) は 8.99 秒でした.

参考

[1] Python MIP Documentation